0308 - 0314
0308
빈 스코프
빈이 존재할 수 있는 범위
- 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
- request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
- session: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
- application: 웹 서블릿 컨텍스와 같은 범위로 유지되는 스코프
스코프 지정
@Scope("prototype")
@Component
public class HelloBean {}
프로토타입 스코프
- 싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환
- 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환
- 핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리
- 프로토타입 빈을 관리할 책임은 빈을 받은 클라이언트에 있으므로 @PreDestroy 같은 종료 메서드는 호출되지 않음
싱글톤 빈과 함께 사용시 문제점
- 싱글톤 빈에서 프로토타입 빈을 주입받아서 사용하는 경우, 싱글톤 빈이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이므로 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성된 것이지, 사용할 때마다 새로 생성되는 것이 아니다.
-> 의도와 다르게 동작
Provider로 문제 해결
싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 어떻게 하면 사용할 때마다 항상 새로운 프로토타입 빈을 생성할 수 있을까?
- 가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.
- 그러나 스프링 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
- 의존관계를 외부에서 주입받는 것이 아니라 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회라고 한다.
ObjectProvider
- 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이다.
@Scope("singleton")
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
prototypeBeanProvider.getObject();을 통해서 항상 새로운 프로토타입 빈이 생성된다.ObjectProvider의getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.
JSR-330 Provider
- 자바 표준을 사용하는 방법으로 ObjectProvider와 비슷하지만
javax.inject:javax.inject:1라이브러리를 gradle에 추가해야 한다.
@Scope("singleton")
static class ClientBean {
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
get메서드 하나로 기능이 매우 단순하다.- 별도의 라이브러리가 필요하고, 자바 표준이므로 다른 컨테이너에서도 사용 가능하다.
실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 드물다.
0309
웹 스코프
- 웹 스코프는 웹 환경에서만 동작한다.
- 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다.
웹 스코프 종류
- request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
- session: HTTP Session과 동일한 생명주기를 가지는 스코프
- application: 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
- websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
- 스프링 애플리케이션을 실행시키면 오류가 발생한다.
- 실행 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않는다.
- 실제 고객의 요청이 와야 생성할 수 있다.
스코프와 Provider
첫 번째 해결방안은 Provider를 사용하는 것이다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
ObjectProvider덕분에myLoggerProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.- 호출하는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다.
스코프와 프록시
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
- 적용 대상이 인터페이스가 아닌 클래스면
TARGET_CLASS를 추가 - 적용 대상이 인터페이스면
INTERFACES를 선택
동작 정리
- CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
- 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 단순한 위임 로직만 있고 싱글톤처럼 동작한다.
특징
- 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯 편리하게 request scope를 사용할 수 있다.
- 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연한다는 점
- 웹 스코프가 아니어도 프록시는 사용할 수 있다.
주의점
- 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다.
- 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자. 무분별하게 사용하면 유지보수하기 어려워진다.
0310
HTTP 헤더 - 캐시와 조건부 요청
캐시 기본 동작
캐시가 없을 때
- 데이터가 변경되지 않아도 계속 네트워크스를 통해서 데이터를 다운로드 받아야 한다.
- 인터넷 네트워크는 매우 느리고 비싸서, 브라우저 로딩 속도가 느려진다.
캐시 적용
- 캐시 덕분에 캐시 가능 시간동안 네트워크를 사용하지 않아도 된다.
- 브라우저 로딩 속도가 매우 빠르다.
- 캐시 유효 시간이 초과하면, 서버를 통해 데이터를 다시 조회하고, 캐시를 갱신한다.
- 이 때 다시 네트워크 다운로드 발생
검증 헤더와 조건부 요청
캐시 유효 시간이 초과해서 서버에 다시 요청 시 두 가지 상황이 나타난다.
- 서버에서 기존 데이터를 변경
- 서버에서 기존 데이터를 변경하지 않음
동작 원리
- 데이터를 전송하는 대신에 저장해 두었던 캐시를 재사용 할 수 있다.
- 단, 클라이언트의 데이터와 서버의 데이터가 같다는 사실을 확인할 수 있는 방법 필요
- 서버에서 데이터를 보낼 때 데이터 최종 수정일 정보를 담아서 보낸다.
- 캐시에 최종 수정일이 저장되어, 추후에 유효기간이 끝나고 새로 요청을 할 때 캐시가 가지고 있는 데이터 최종 수정일을 보내서 서버에 있는 데이터 최종 수정일과 비교한다.
- 바뀌지 않았으면 304 Not Modifed를 결과로 주어서 Http 헤더만 전송한다. (Http 바디 전송 X)
- 응답 결과를 재사용하여 헤더 데이터를 갱신한다.
- 결과적으로 네트워크 다운로드가 발생하지만 용량이 적은 헤더 정보만 다운로드하여 캐시에 저장되어 있는 데이터를 재활용할 수 있다.
- 조건이 만족하여 데이터가 변경되었으면 200 OK와 함께 데이터를 다시 보내서 캐시에 저장한다.
조건부 요청 해더
- If-Modified-Sincd: Last-Modified 사용
- If-None-Match: ETag 사용
- If-Modified-Sincd, Last-Modified는 데이터 최종 수정일로 변경을 감지한다면, ETag는 데이터가 변경되면 이 이름을 바꾸어서 변경한다.
- 단순하게 ETag만 보내서 같으면 유지, 다르면 다시 받을 수 있다.
- ETag는 캐시 제어 로직을 서버에서 완전히 관리하고, 클라이언트는 단순히 이 값을 서버에 제공한다.
캐시 제어 헤더
Cache-Control
- Cache-Control: max-age
- 캐시 유효 시간, 초 단위
- Cache-Control: no-cache
- 데이터는 캐시해도 되지만 항상 origin 서버에 검증하고 사용
- Cache-Control: no-store
- 데이터에 민감한 정보가 있으므로 저장하면 안 되고, 메모리에서 사용하고 최대한 빨리 삭제
프록시 캐시
예를 들어 미국에 있는 origin 서버에서 데이터를 받아온다면 긴 시간이 걸리기 때문에, 한국 어딘가에 프록시 캐시 서버를 두어서 더 빠르게 데이터를 받아올 수 있다.
- Cache-Control: public
- 응답이 public(프록시) 캐시에 저장되어도 됨
- Cache-Control: private
- 응답이 해당 사용자만을 위한 것임 (기본 값)
캐시 무효화
웹 브라우저가 임의로 캐시를 저장해버릴 수 있기 때문에 확실한 캐시 무효화 응답이 필요한 경우에 지정해 줘야 한다.
- Cache-Control: no-cache, no-store, must-revalidate
- Pragma: no-cache
Cache-Control: must-revalidate
- 예를 들어 no-cache는 프록시 캐시에서 네트워크가 단절되어 origin 서버에 접근이 불가한 경우 캐시 서버 설정에 따라 오류보다는 오래된 데이터라도 보여줄 수 있게 지정되어 있는 반면, must-revalidate는 origin 서버에 접근할 수 없는 경우 항상 504 오류가 발생하게 한다.
0311
Gradle에서 Spring Boot - MySQL - Mybatis 연동 및 설정
1. gradle에 의존성 추가
// build.gradle
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'
runtimeOnly 'mysql:mysql-connector-java'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'를 설정해줘야만 데이터베이스 연결이 되는 줄 알았는데, 없어도 정상 작동하는 것을 보니 mybatis를 구현하면 자동으로 jdbc도 구현해주는 것 같다.- runtimeOnly: runtime 시에만 필요한 라이브러리인 경우
- compileOnly: compile 시에만 빌드하고 빌드 결과물에는 포함하지 않음
2. MySQL, mybatis 관련 apelication.properties 설정
// apelication.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://ip주소:포트번호/DB이름?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
spring.datasource.username=사용자
spring.datasource.password=비밀번호
mybatis.type-aliases-package=jh.SimpleBoard.domain
mybatis.mapper-locations=classpath:mybatis/query/*.xml
mybatis.config-location=classpath:mybatis/mybatis-config.xml
mybatis.map-underscore-to-camel-case=true
3. DB에 쿼리를 날릴 Mapper 작성
// *.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="매핑할 mapper 경로">
<!-- DML 작성 -->
</mapper>
0312
slf4j + log4j2 설정
의존성
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
testImplementation 'org.springframework.boot:spring-boot-starter-log4j2'
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16' // sql을 로그에 나타내기 위해 추가
configurations {
// logBack 의존성 제거
all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
}
- 기본적으로 Spring은 Slf4j라는 구현체를 손쉽게 교체할 수 있도록 도와주는 로깅 프레임워크를 사용한다.
- Slf4j는 인터페이스고 내부 구현체로 logback을 가지고 있으므로, log4j2를 사용하기 위해 exclude 해준다.
설정 파일
// log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
<Properties>
<Property name="consoleLayout">%style{ %d{ISO8601}}{black } %highlight{ %-5p }{FATAL=bg_red, ERROR=red, INFO=green, DEBUG=blue}%l: %msg%n%throwable</Property>
</Properties>
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="${consoleLayout}" />
</Console>
</Appenders>
<Loggers>
<Root level="info" additivity="false">
<AppenderRef ref="console"/>
</Root>
<!-- log4jdbc 옵션 설정 -->
<logger name="jdbc" level="OFF"/>
<!-- 커넥션 open close 이벤트를 로그로 남긴다. -->
<logger name="jdbc.connection" level="OFF"/>
<!-- SQL문만을 로그로 남기며, PreparedStatement일 경우 관련된 argument 값으로 대체된 SQL문이 보여진다. -->
<logger name="jdbc.sqlonly" level="OFF"/>
<!-- SQL문과 해당 SQL을 실행시키는데 수행된 시간 정보(milliseconds)를 포함한다. -->
<logger name="jdbc.sqltiming" level="DEBUG"/>
<!-- ResultSet을 제외한 모든 JDBC 호출 정보를 로그로 남긴다. 많은 양의 로그가 생성되므로 특별히 JDBC 문제를 추적해야 할 필요가 있는 경우를 제외하고는 사용을 권장하지 않는다. -->
<logger name="jdbc.audit" level="OFF"/>
<!-- ResultSet을 포함한 모든 JDBC 호출 정보를 로그로 남기므로 매우 방대한 양의 로그가 생성된다. -->
<logger name="jdbc.resultset" level="OFF"/>
<!-- SQL 결과 조회된 데이터의 table을 로그로 남긴다. -->
<logger name="jdbc.resultsettable" level="DEBUG"/>
</Loggers>
</Configuration>
- Configuration: 로그 설정의 최상위 요소. 일반적으로 Properties, Appenders, Loggers 요소를 자식으로 가짐
- status 속성은 log4j의 내부 이벤트에 대한 로그 레벨을 의미
- Properties: Configuration 파일에서 사용할 변수. name 속성의 값이 key, 태그 내의 값이 value가 된다.
- Layout: Appender는 로그 이벤트를 Layout을 통해 포매팅 한다. Appender가 받은 로그 이벤트를 Layout에게 전달하면, byte 배열을 반환
- ex) %style{ %d{yyyy/MM/dd HH:mm:ss,SSS} }{cyan} %highlight{[%-5p]}{FATAL=bg_red, ERROR=red, INFO=green, DEBUG=blue} [%C] %style{[%t]}{yellow}- %m%n -
- Appender: log 메세지를 특정 위치에 전달해주는 역할을 한다.
- layout을 통해 로그를 formatting 하고, 어떤 방식으로 로그를 제공할지 결정
- Logger: 로깅을 직접 하는 요소. 패키지 별로 설정 가능
- Root 패키지의 로거는 필수적이고, 추가적인 로거는 Logger로 설정 가능
application.properties
// application.properties
logging.config=classpath:log4j2.xml
jdbc spy 로깅 이벤트를 slf4j가 받도록 처리
// log4jdbc.log4j2.properties
# log4jdbc spy의 로그 이벤트를 slf4j를 통해 처리한다.
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
# 로그를 표시할 줄의 제한, 0은 무제한
log4jdbc.dump.sql.maxlinelength=0
# log4jdbc의 드라이브 클래스 설정
log4jdbc.drivers=com.mysql.cj.jdbc.Driver
log4jdbc.auto.load.popular.drivers=false
0313
인터셉터
클라이언트로부터 요청이 들어오면, Filter가 있다면 Filter를 수행한 후, DispatcherServlet을 수행한다. 즉, 필터는 요청이나 응답이 처리되기 전 거치는 것
- DispatcherServlet은 선처리 작업할 것이 존재하면 선처리 작업을 해주고, HandlerMapping을 통해 실제 어떤 Handler가 동작을 해야 되는지 알아내고, Handler를 실행시킨다.
- DispatcherServlet와 Handler 사이에 HandlerInterceptor라고 하는 것이 있다.
- 인터셉터는 DispatcherServlet에서 Handler로 요청을 보낼 때, Handler에서 DispatcherServlet로 응답을 보낼 때 동작한다.
- 인터셉터로 컨트롤러 실행 전과 후에 공통 로직을 처리할 수 있다.
인터셉터 작성하는 방법
- org.springframework.web.servlet.HandlerInterceptor 인터페이스를 구현한다.
- preHandle, postHandle 메서드를 오버라이드 해서 공통 로직을 정의한다.
public class BaseHandlerInterceptor implements HandlerInterceptor {
Logger logger = LoggerFactory.getLogger(getClass());
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.info("preHandle requestURI : {}", request.getRequestURI());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
logger.info("postHandle requestURI : {}", request.getRequestURI());
}
}
- Java Config를 사용한다면, WebMvcConfigurer가 가지고 있는 addInterceptors 메서드를 오버라이딩 하고 등록하는 과정을 거친다.
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Bean
public BaseHandlerInterceptor baseHandlerInterceptor() {
return new BaseHandlerInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(baseHandlerInterceptor());
}
}
0314
enum 활용
Enum class
상수를 정의하여 코딩하는 경우의 문제점들을 보완하기 위해 새로 추가한 것으로, 열거형이라고 불리며 서로 연관된 상수들의 집합
장점
- 코드가 단순해지며, 가독성이 좋다.
- 인스턴스 생성과 상속을 방지하여 상수값의 타입안정성이 보장한다.
- enum class를 사용해 새로운 상수들의 타입을 정의함으로 정의한 타입이외의 타입을 가진 데이터값을 컴파일시 체크한다.
- enum 키워드를 사용하기 때문에 구현 의도가 열거임을 분명하게 알 수 있다.
선언
public class DevType {
MOBILE, WEB, SERVER
}
- 열거형으로 선언된 순서에 따라 0부터 인덱스 값을 가진다. 순차적으로 증가
- enum 열거형으로 지정된 상수들은 모두 대문자로 선언
- 열거형 변수들을 선언한 후 세미콜론은 찍지 않는다. 그러나 상수와 연관된 문자를 연결시킬 경우 세미콜론을 찍는다.
Enum 메소드
- values(): 열거된 모든 원소를 배열에 담아 순서대로 리턴
- ordinal(): 원소에 열거된 순서를 정수 값으로 리턴
- valueOf(): 매개변수로 주어진 String과 열거형에서 일치하는 이름을 갖는 원소를 리턴
열거형 상수를 다른 값과 연결하기
- 열거형 상수와 관련된 값을 생성자를 통해 연결시킬 경우 세미콜론을 붙여야 함
enum DevType {
//상수("연관시킬 문자") <- 이땐 줄 끝에 세미콜론 (;) 붙이기.
MOBILE("안드로이드"), WEB("스프링"), SERVER("리눅스");
final private String name;
public String getName() {
return name;
}
private DevType(String name){
this.name = name;
}
}
- 생성자를 private로 막아줘야 다른곳에서 동적으로 변화시킬 수 없게끔 한다.