Skip to content

Latest commit

 

History

History
2139 lines (1358 loc) · 85.3 KB

SpringBoot+JPA.md

File metadata and controls

2139 lines (1358 loc) · 85.3 KB

SpringBoot + JPA + ...


@SpringBootApplication

스프링부트 템플릿을 통해 프로젝트를 생성하면 아래와 같이 자동으로 xxxApplication에 메인메소드가 생성이 된다.

@SpringBootApplication
public class FileDbWorkApplication {

	public static void main(String[] args) {
		SpringApplication.run(FileDbWorkApplication.class, args);
	}

}

그 곳에 **@SpringBootApplication**이 존재하게 되며 그 안을 확인해보면

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    ...

여러 어노테이션으로 설정되어있는 것을 볼 수 있는데, 여기서 중요한 것은 @ComponentScan이랑 EnableAutoConfiguration이다.

@ComponentScan

@SpringBootApplication 어노테이션이 붙은, 즉 컴포넌트 스캔이 선언된 하위 패키지에서

@Component @Configuration @Controller @RestController @Service @Repository

와 같은 Annotation이 선언된 클래스를 스프링 컨테이너의 메모리로 읽어와 애플리케이션에서 사용할 수 있는 Bean 형태로 등록하게 된다.

@EnableAutoConfiguration

컴포넌트 스캔 이후 EnableAutoConfiguration으로 추가적인 Bean들을 읽어서 등록한다.

spring.factories 내부에 여러 Configuration들이 있고, 조건에 따라서 Bean을 등록한다.

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
...
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
...

org.springframework.boot.autoconfigure.EnableAutoConfiguration라는 Key 값으로 많은 Class를 가지고 있다. 해당 클래스들은 상단에 @Configuration이라는 Annotation을 가지고 있어 이러한 키값을 통해 명시된 많은 클래스들이 AutoConfiguration의 대상이 된다.

Spring Bean LifeCycle

Spring Bean이라고 하는 것은 일반적으로 객체인데, IoC 컨테이너가 관리하는 객체를 Bean이라고 한다.

( new로 만드는 객체는 Bean이 아니다 )

대략적으로 스프링 빈의 life cycle은 아래와 같다.

# 객체 생성 > 의존 설정 > 초기화 > 소멸 단계

스프링 컨테이너를 초기화 할 때 스프링 컨테이너는 빈 객체를 생성하게 되고 이 시점에 DI를 통한 의존성이 주입된다.

모든 의존 설정이 완료되면 빈 객체를 초기화 하기 위해서 스프링은 빈 객체의 지정된 메서드를 호출하게 된다.

마지막으로 스프링 컨테이너가 close() 메서드로 종료될 시점에 컨테이너는 빈 객체의 소멸을 처리하고 이때도 지정된 메서드를 호출하게 된다.

그럼 어떻게 Spring Container안에 특정한 인스턴스를 빈으로 만들 수 있는가?

  1. Component-Scan
  2. 직접 Bean으로 등록

IoC 컨테이너가 사용하는 여러 인터페이스가 있는데, 그런 인터페이스들을 lifecycle callback 이라고 한다.

그 여러 Lifecycle CallBack 중에서는 @Component로 선언된 클래스를 빈으로 등록하는 처리기가 등록되어 있다.

그것이 바로 @SpringBootApplication 안에서 @ComponentScan이 알려주는 위치의 모든 하위패키지부터 훑어보면서 @Component로 등록된 클래스를 빈으로 등록한다!

특정한 Repository를 상속받을 때 인터페이스 안에서 내부적으로 Bean을 만들어서 관리한다.

@Configuration
public class SampleConfig {
    
    @Bean
    public SampleController sampleController() {
        return new SampleController();
    }
}

@Configuration도 Component이기 때문에 Bean으로 등록되고

위와 같이 @Controller를 사용하지 않고 직접 Controller를 만들어서 Bean으로 등록할 수 도 있다.

그러면 이제 스프링 빈의 초기화 및 종료 방법 3가지를 알아보겠다.(Spring Bean Lifecycle Callback)

1. 인터페이스 활용

InitializingBean, DisposableBean 인터페이스를 활용한다.

public class CustomClient implements InitailizingBean, DisposableBean {
    private String url;
    
    public CustomClient() {
        System.out.println("생성자 호출, url: " + url);
    }
	public void setUrl(String url) {
        this.url = url;
    }

    // 애플리케이션 시작 시 호출
	public void connect() {
        System.out.println("연결 url: " + url);
    }
    
    // 서비스 중에 호출
    public void call(String message) {
        System.out.println("url: " + url + "/message = " + message);
    }
    
    // 서비스 종료 시 호출
    public void disconnect() {
        System.out.println("서비스 종료");
    }
    
	// 빈 생성 후 의존관계 주입이 완료되고 호출된다.
	@Override
	public void afterPropertiesSet() throws Exception {
        connect();
    }
    
    // 스프링 컨테이너 종료 직전에 호출된다.
    @Override
    public void destroy() throws Exception {
        disconnect();
    }
}
public class BeanTest {
    @Test
    public void beanTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig.class);
        CustomClient client = ac.getBean(CustomClient.class);
        ac.close();
    }
    
    @Configuration
    static class MyConfig {
        @Bean
        public CustomClient customClient() {
            CustomClient customClient = new CustomClient();
            customClient.setUrl("http://...");
            return customClient;
        }
    }
}

위와 같이 커스텀 빈을 만들고, 이 객체에 InitializingBean, DisposableBean 인터페이스를 장착하면 빈 생명주기를 관리해주는 콜백함수들을 사용할 수 있다.

이렇게 작성하고 실제로 Configuration에서 빈을 등록하고 사용하면 의존관계가 주입 된 후에 afterPropertiesSet()으로 connect()와 call() 메소드가 호출됨을 확인할 수 있다.

하지만 이처럼 인터페이스로 콜백을 사용하는 방법은 스프링 전용 인터페이스이며, 스프링에 의존적이고 외부 라이브러리에 적용할 수 없다는 단점이 있다.

2. 빈 등록 시 초기화/소멸 메서드 등록

Bean을 등록할 때 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.

public class BeanTest {
    @Test
    public void beanTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig.class);
        CustomClient client = ac.getBean(CustomClient.class);
        ac.close();
    }
    
    @Configuration
    static class MyConfig {
        // 빈 설정 시 초기화, 소멸 메서드를 지정해준다.
        @Bean(initMethod = "init", destroyMethod = "close")
        public CustomClient customClient() {
            CustomClient customClient = new CustomClient();
            customClient.setUrl("http://...");
            return customClient;
        }
    }
}

CustomClient.class 에 지정해줬던 함수명들로 init, close method를 작성한다.

public class CustomClient implements InitailizingBean, DisposableBean {
    /*
    	위는 모두 동일
    */
    
	public void init() throws Exception {
        connect();
    }
    
    public void close() throws Exception {
        disconnect();
    }
}

첫 번째 방법과 같은 결과를 도출할 수 있따.

여기서 특이한 점은 Spring이 @Bean의 종료메서드에 대해서 임의로 추론해서 자동으로 호출해주는 기능이 있다.

@Bean(initMethod = "init", destroyMethod = "close")
->
@Bean(initMethod = "init")

종료메서드 지정을 없어도 같은 기능을 하는 것을 볼 수 가 있는데, 이는 등록된 Bean 안에 일반적으로 많이 쓰는 종료 메서드 들의 이름 (ex. close, shutdown, ...)들의 메서드가 있으면 스프링이 종료될 때 자동으로 호출해준다! 와 신기

추론기능을 사용하지 않으려면 destroyMethod = ""로 공백을 쓰면 된다.

3. Annotation Callback

콜백으로 등록하고 싶은 메소드에 어노테이션을 달아서 Bean의 콜백 메서드로 등록하는 방법이다.

// CustomClient.class

@PostConstruct
public void init() throws Exception {
    connect();
}

@PreDestroy
public void close() throws Exception {
    disconnect();
}

CustomClient라는 Bean 안에 init과 close를 콜백으로 등록하고 싶을 때 각각

@PostConstruct, @PreDestroy 어노테이션을 사용하면 편리하게 초기화와 종료를 실행할 수 있다.

이 방법은 최신 스프링에서 가장 권장하는 방법으로 매우 편리하게 쓸 수 있다.

또한, 스프링에 종속적인 기술이 아니라 자바 표준이므로 다른 컨테이너에서도 동작가능하다.

하지만 외부 라이브러리에 적용하지 못한다는 점이 있는데 이 때는 @Bean의 기능을 사용하면 될 것 같다.

참조 : https://chung-develop.tistory.com/55

Autowiring

IoC 컨테이너에 등록된 빈을 어떻게 꺼내서 사용할 것인가 ?

스프링에는 다양한 의존성 주입 방법이 있는데,

생성자로 직접 주입을 받아도 되지만 @Autowired를 사용하면 IoC 컨테이너에 들어있는 Bean을 주입받아서 사용할 수 있다.

스프링 컨테이너는 협력관계에 있는 빈들에 대해서 자동으로 관계를 주입한다.

ApplicationContext의 내용을 조사한 뒤에 Spring이 빈에 대해서 자동으로 다른 연관관계 빈에 대해서 의존성을 주입할 수 있도록 할 수 있다.

Autowire을 사용하면 다음과 같은 이점을 가진다.

  • 특정 프로퍼티나 생성자의 argument를 지정할 필요성을 크게 줄일 수 있다.
  • 클래스가 발전함에 따라서 구성의 업데이트를 쉽게 할 수 있다. 이 말은, 클래스에 종속성을 추가하는 경우 구성을 수정할 필요없이 해당 종속성이 자동으로 충족된다. 따라서 이 Autowiring은 개발 중에 매우 유용하다.

XML 기반의 환경설정을 사용한다면 autowire 속성으로 빈 정의에 대한 자동 연결 모드를 지정할 수 있다.

4가지 모드가 있고, 개발자는 각 빈에 대해서 autowiring을 지정하고 선택할 수 있다.

  • no
    • 기본값으로 Autowiring이 되지 않는다.
    • Bean references는 반드시 ref에 의해 정의되어야 하는 상태.
    • 명시적으로 공동 작업자를 지정하면 더 나은 제어와 명확성을 제공하므로 대규모 배포에는 기본 설정을 변경하지 않는 것이 좋다.
  • byName
    • 속성의 이름으로 자동 연결된다.
    • Spring은 자동으로 연결되어야 하는 속성과 이름이 같은 빈을 찾는다.
    • 예를 들어서 bean definition이 이름에 의해 autowire로 설정되고 master 속성을 포함 하는 경우(즉, setMaster(..) 메서드가 있는 경우) Spring은 이름이 지정된 bean definition의 master를 찾아서 속성을 설정하게된다.
  • byType
    • 속성의 타입의 bean이 컨테이너에 정확히 하나만 존재하는 경우에 속성이 자동으로 연결된다.
    • 둘 이상이 있다면 치명적인 예외가 발생하여 bean에 대해서 byType으로 autowiring할 수 없음을 나타낸다.
    • 일치 하는 빈이 없다면 아무 일도 일어 나지 않는다.(속성이 설정되지 않는다.)
  • constructor
    • byType과 유사하지만 생성자에 인수가 적용된다.
    • 컨테이너에 해당 생성자 유형의 빈이 정확히 하나 이상 없으면 치명적인 에러가 발생한다.

@Autowired

우리가 @Autowired로 의존성을 주입하는 경우 default로 byType의 모드로 동작된다. 만약 특정한 이름을 사용하고 싶으면 @Qualifer 어노테이션을 사용하면 된다.

Spring 4.3부터 어떠한 클래스에 생성자가 하나 뿐이고, 그 생성자가 주입받는 레퍼런스 변수들이 Bean으로 등록되어 있다면 그 Bean을 자동으로 주입하는 기능으로, @Autowired를 생략할 수 있다.

@Qualifer, @Primary

의존관계를 주입할 빈의 후보개 여러개라면 해결할 수 있는 세 가지 방법이 있다.

  1. @Autowried의 필드 매칭

    • Spring 5에서 추가된 바로는 매칭된 타입이 둘 이상일 경우에 Spring framwork는 적절한 후보를 찾기 위해서 필드명을 빈 이름으로 사용한다. (DefaultListBeanFacotory)
    • 의존성 주입을 받을 필드이름을 구현체의 이름으로 명시해서 찾는 방법이다.
    @Autowired
    private final OrderRepository orderRepository;
    • 이렇게 필드의 이름을 인터페이스가 아닌 실제 원하는 구현체의 이름을 적용시키는 것.
    • 하지만 추천하지 않고 @Primary@Qualifer를 적절히 사용하는 것이 권장된다.
  2. @Qualifer

    • @Qualifer는 여러개의 타입이 일치하는 bean객체가 있을 경우에 @Qualifer 어노테이션의 유무에 따라서 조건에 만족하는 객체를 주입하게 된다.
    • 선택되는 구현체들이나 사용하는 코드에 @Qualifer("찾는 이름")을 작성해서 주입받을 빈을 찾는다.
    @Component
    @Qualifer("mainDiscountPolicy")
    @Primary // 우선순위 사용예시 (rate가 아니라 fix를 먼저 찾게된다.)
    public class FixDiscountPolicy implements DiscountPolicy {
        private int discountFixAmount = 1000;
        
        @Override
        public int discount(Member meber, int price) {
            if (member.getGrade() == Grade.VIP) {
                return discountFixAmount;
            }
            return 0;
        }
    }
    @Component
    @Qualifer("subDiscountPolicy")
    public class RateDiscountPolicy implements DiscountPolicy {
        private int discountPercent = 10;
        
        @Override
        public int discount(Member member, int price) {
            if (member.getGrade() == Grade.VIP) {
                return price * discountPercent / 100;
            }
            return 0;
        }
    }
    // 활용 코드
    public class OrderServiceImpol implements OrderService {
        private final MemberRepository memberRepository;
        private final DiscountPolicy discountPolicy;
        
        public OrderServiceImpl(MemberRepository memberRepository, @Qualifer("mainDiscountPolicy") DiscountPolicy discountPolicy) {
            this.memberRepository = memberRepository;
            this.discountPolicy = discountPolicy;
        }
    }
  3. @Primary

    • @Primary 어노테이션으로 우선순위를 지정할 수 있다.
    • 같은 타입의 빈을 찾을 때 @Priamry가 붙은 빈을 우선적으로 찾게 된다.
    • 실무에서 많이 사용하는 방법!

참조 : https://velog.io/@neity16/Spring-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8-8-Primary-Qualifier

Autowiring의 한계와 단점

Autowiring은 프로젝트 전체에서 일관되게 사용되어야 한다.

일관적으로 사용되지 않을 경우, 개발자가 빈 정의들을 연결하는 것에 혼란을 겪게 된다.

Autowiring에는 아래와 같은 한계와 단점이 존재한다.

  • propertyconstructor-arg으로 명시적 종속하는 것은 항상 Autowiring을 재정의한다. Primitive, String, Classes과 같은 단순 속성은 Autowiring할 수 없다. 의도적으로 설계된 것.
  • Autowiring은 명시적으로 주입하는 것보다 덜 정확하다. Spring이 예상치 못한 결과를 초래할 수 있는 모호한 경우를 대비해서 주의해야 한다.
  • 컨테이너 내의 여러 빈 정의는 자동 연결될 setter 메소드 뜨는 constructor-arg에 의해 지정된 유형과 일치할 수 있다. 배열, 컬렉션, 또는 Map 인스턴스의 경우 반드시 문제가 되는 것은 아니지만 단일 값을 기대하는 종속성의 경우 이 모호성이 임의로 해결되지 않는다. 사용 가능한 고유 빈 정의가 없으면 예외가 발생한다.

이를 위해 개발자에게는 다음과 같은 옵션이 주어진다.

  • 명시적 연결을 위해서 Autowiring을 포기하기
  • autowire-candidate 속성을 false로 설정하여 빈 정의에 대한 Autowiring을 방지하기
  • <bean/>primary 속성을 true로 설정하여 단일 빈정의를 기본으로 설정
  • annotation-based configuration을 기반으로 보다 세분화된 제어를 구현

Bean Scope

Bean을 정의할 때 해당 빈에 정의된 클래스의 실제 인스턴스를 생성하기 위한 방법을 생성한다.

단일 레시피로 많은 객체 인스턴스를 만들 수 있기 때문에 Bean Definition에 관한 개념은 매우 중요하다.

스프링은 기본적으로 모든 Bean을 Singleton으로 생성해 관리한다.

구체적으로는 애플리케이션 구동시 JVM 안에서 스프링이 bean마다 하나의 객체를 생성하는 것을 의미하고, 우리는 스프링을 통해서 bean을 제공받으면 주입 받은 bean은 언제나 동일한 객체임을 가정하고 개발한다.

특정 Bean Definition에서 생성된 객체에 연결될 다양한 종속성과 구성을 제어할 수 있을 뿐만 아니라, 특정 Bean Definition에서 생성된 객체의 범위도 제어할 수 있다.

1. singleton

singleton bean은 스프링 컨테이너에서 단 한 번 생성이되고 컨테이너가 종료될 때 bean도 소멸된다.

생성된 하나의 인스턴스는 single beans cache에 저장되고, 해당 bean에 대한 요청과 참조가 있으면 캐시된 객체를 반환한다. 즉, 하나만 생성되기 때문에 모두 동일한 것을 참조한다.

기본적으로 모든 bean은 scope를 명시적으로 지정하지 않으면 singleton이다.

<bean id="accountService" class="com.something.DefaultService" scope="singleton"/>
@Scope("singleton")

추가적으로, 스프링에서 말하는 싱글톤은 디자인패턴의 싱글톤패턴과 다르다.

싱글톤패턴은 하드코딩된 객체(classloader)가 singleton 객체를 관리하므로 사용시 유의해야 할 점이 있다.

singleton 객체가 전역변수와 같은 역할 때문에 coupling이 높아서 테스트에 어려움이 있고 애플리케이션의 의존성 측면에서도 singleton 객체를 사용하는 쪽에서 singleton 객체에 대해 너무 많은 정보를 알아야 한다는 설계상의 문제가 있다.(참고)

2. prototype

prototype bean은 모든 요청에 대해 새로운 객체를 생성한다.

즉, 의존성 관계의 bean에 주입될 때 새로운 객체가 생성되어 주입되고 정상적인 방식으로 gc에 의해 bean이 소멸된다.

일반적으로 모든 Stateful Bean에는 프로토타입 scope을 사용하고, Stateless Bean에는 싱글톤 scope을 사용해야 한다.

Spring은 prototype bean의 생명 주기를 관리하지 않는다. 컨테이너는 프토토타입 객체를 인스턴스화 해서 클라이언트에 전달한다.

따라서 초기화에 대한 lifecycle callback method는 scope에 관계없이 모든 객체에 대해 호출되지만 prototype의 경우 소멸 lifecycle callback은 호출되지 않는다.

그러므로 클라이언트 코드는 프로토타입 scope 인스턴스를 정리하고 프로토타입 빈이 보유하고 있는 resource를 해제해주어야 한다. 해당 빈에 대한 참조를 보유하고 있는 Custom Bean Post Processor를 이용해서 프로토타입의 빈 리소스를 해제할 수 있다.

<bean id="accountService" class="com.something.DefaultService" scope="prototype"/>
@Scope("prototype")

3. singleton with prototype-bean dependencies

프로토타입 빈에 대한 종속성이 있는 싱글톤 빈은, 인스턴스시킬 때 해결된다.

프로토타입의 빈을 -> 싱글톤 빈으로 의존성을 주입하면 새로운 프로토타입 빈이 인스턴스화되고 싱글톤 빈에 종속성이 주입된다. 따라서 이 프로토타입 인스턴스는 싱글톤 빈에 제공되는 유일한 인스턴스이다.

Spring컨테이너는 싱글톤 빈을 인스턴스화하고 의존성을 해결하고 주입할 때 이 DI가 한번만 발생하기 때문에, 프로토타입 빈을 반복적으로 싱글톤 빈에 종속할 수 없다. 런타임시 프로토타입 빈의 새로운 인스턴스가 두 번이상 필요하다면 Method Injection을 사용하자.

4. Request, Session, Application, WebSocket Scopes

request, session, application, websocket scope는 스프링의 ApplicationContext의 구현체를 사용하는 경우에만 사용가능하다.(XmlWebApplicatoinContext) 즉, Spring MVC Web Application에서만 사용되는 용도이다.

이러한 Bean scope를 일반적인 Spring IoC 컨테이너(ClassPathXmlApplicationContext)와 함께 사용하면 IllegalStateException과 같은 알수없는 예외가 발생한다.

  • request : HTTP 요청 별로 인스턴스화 되며 요청이 끝나면 소멸된다.
  • session : HTTP 세션 별로 인스턴스화 되며 세션이 끝나면 소멸된다.

> singleton vs non-singleton

싱글톤과 비싱글톤을 구분해서 빈을 선언하는 기준의 핵심은 수정 가능한 상태에 따른 동기화 비용과 객체 생성 비용간의 트레이드 오프를 따져 보라는 의미

  • 싱글톤으로 적합한 객체

    • 상태가 없는 공유 객체

      : 상태를 가지고 있지 않은 객체는 동기화 비용이 없어 매번 객체를 참조하는 곳에서 새로운 객체를 생성할 이유가 없다.

    • 읽기용으로만 상태를 가진 공유 객체

      : 1번과 유사하개 상태를 가지고 있으나 읽기전용이므로 여전히 동기화 비용이 들지 않는다. 따라서 매 요청마다 새로운 객체를 생성할 필요가 없다.

    • 공유가 필요한 상태를 지닌 공유 객체

      : 객체간에 반드시 공유해야 할 상태를 지닌 객체가 하나 있다면, 이 경우에는 해당 상태의 쓰기를 가능한 동기화할 경우 싱글톤도 적합하다.

    • 쓰기가 가능한 상태를 지니면서도 사용빈도가 매우 높은 객체

      : 애플리케이션에서 사용빈도가 높다면 쓰기 접근에 대한 동기화 비용을 감안하고서라도 싱글톤을 고려할만 하다.

      : 이 방법은 1.장시간에 걸쳐 매우 많은 객체가 생성되고, 2.해당 객체가 매우 작은 양의 쓰기상태를 가지고 있고, 3.객체 생성비용이 매우 클때 유용한 선택이 된다.

  • 비싱글톤(ex. 프로토타입)으로 적합한 객체

    • 쓰기가 가능한 상태를 지닌 객체

      : 쓰기가 가능한 상태가 많아서 동기화 비용이 객체 생성 비용보다 크다면 싱글톤으로 적합하지 않다.

    • 상태가 노출되지 않는 객체

      : 일부 제한적인 경우, 내부 상태를 외부에 노출하지 않는 빈을 참조하여 다른 의존객체와는 독립적으로 작업을 수행하는 의존객체가 있다면 싱글톤보다 비싱글톤 객체를 사용하는 것이 더 좋을수도 있다.

참조 : https://gmlwjd9405.github.io/2018/11/10/spring-beans.html

Method Injection

생명주기가 다른 두 빈에 대한 작업을 할 때 사용하는 Injection 방법

Singleton Bean이 Prototype Bean의 참조를 가지고 있어서 Prototype이어야 하는 객체가 싱글톤으로 동작하는 문제를 해결하기 위해 생겨난 것

<bean id = "someBean" class = "com.SomeBean" scope = "prototype"/>

<bean id = "singleBean" class "com.SingleTone">
	<property name = "mySomeBean">
    	<ref bean = "someBean"/>
    </property>
</bean>

위와 같은 경우에 Spring은 singleton 객체를 리턴할 때 최초 한번 생성된 bean의 인스턴스를 계속해서 리턴하게 되는데 위와 같이 정의된 경우에는 singleBean은 singleton 패턴으로 초기 생성 이후 소멸하지 않기 때문에 singleBean 내부에 가지고 있는 someBean 역시 최초에 만들어지고, 다시 만들어지지 않게 되는 현상이 발생한다.

나는 someBean을 prototype으로 사용하고 싶다고!!

사실 setter로 인스턴스를 주입받고, getter에서 new 키워드로 someBean에 대한 새로운 인스턴스를 리턴하면 되긴 하지만, 이러한 해결책은 IoC에 어긋난다. 이렇게 하면 bean으로 관리할 필요가 없는 것이다.

고로, IoC를 유지하면서 위와같은 문제를 해결하기 위해서 Method Lookup injection이 필요하다.

@Lookup

이 방법은 런타임에 Spring Bean을 재정의하는 프로세스이다.

먼저, 사용하려는 프로토타입 클래스를 하나 생성하고

@Component
@Scope("prototype")
public class PlaceOrderNotification {
    
    public String getNotification() {
        return "Order Placed";
    }
}

싱글톤 빈에서 프로토타입 빈에 접근한다.

@Component
public class OrderService {
    
    public void sendOrderNotificationMsg() {
        PlaceOrderNotification notification = getPlaceOrderNotification();
    }
    
    @Lookup
    public PlaceOrderNotification getPlaceOrderNotification() {
        /*
        여기서 Spring은 런타임시에 동적으로 메소드를 재정의하게 됩니다.
        */
        return null;
    }
}

현재 위치에서는 null을 반환하지만 Spring은 런타임시 동적으로 @Lookup 어노이션이 붙은 메소드를 오버라이드하게 된다. 이러한 Method Injection 방법은 Spring에 의존적이지도 않지만, 단위 테스트를 작성할 때 번거롭다는 점이 있다.

public PlaceOrderNotification getPlaceOrderNotification() {
    return applicationContext.getBean(PlaceOrderNotification.class);
}

@Lookup 어노테이션을 쓸때 주의할 점은

  1. Bean class는 final일 수 없고,
  2. @Lookup이 달린 Method는 private, static, final 모두 올 수 없다.

위에서 봤던 테스트에서 발생하는 번거로운 점 때문에, Spring은 이런 경우 빈을 찾아야 하는 경우 다음의 방법을 권장한다고 한다.

Provider

만약, 원하는 객체를 찾기 위해서 의존성을 검색해야 한다면 Provider를 사용해보자.

Provider는 JSR-330에 추가된 자바 표준으로 <T> 타입 파라미터와 get()이라는 팩토리메서드를 갖는 인터페이스이다.

@RestController
@RequiredArgsConstructor
public class TestController {
    
    private final Provider<TestService> provider;
    
    @GetMapping("/test")
    public ResponseEntity<TestService> test() {
        final TestService testService = provider.get();
        return ResponseEntity.ok(testService);
    }
}

음. Provider는 조금 더 공부해봐야겠다

Lombok

Lombok은 자바의 반복 메서드 작성 코드를 줄여주는 코드 다이어트 라이브러리이다.

보통 DTO, Model, Entity의 경우 여러 속성이 존재하고 이들의 프로퍼티에 대해서 Getter, Setter, 생성자 등 매번 작성해줘야 하는 부분을 자동으로 만들어주는 라이브러리이다.

코딩과정에서는 어노테이션만 보이지만, 실제 컴파일된 .class 파일에는 연관 코드들이 생성되어 있게 된다.

귀찮은 과정을 줄여주고 반복되는 코드작성을 대신하기 때문에 많은 개발자들이 사용하고 있지만 호불호가 갈리는 라이브러리이기도 하며, 개별 작동방식을 잘 알고 사용하는 것이 좋다.

  • @Getter, @Setter

    • @Getter와 @Setter를 클래스 이름 위에 적용 시키면 모든 non-static 변수들에 대한 getter, setter 메소드를 사용할 수 있고, 변수이름 위에 따로 사용할 수도 있다.
    • 자동으로 생성되는 메소드의 기본은 public이며 AccessLevel을 통해 명시적으로 생성할 수도 있다.
    • 열거형 변수에는 getter를 사용할 수 있지만, setter를 사용할 수 없다.
    • 이름이 같고 매개변수의 수가 같은 메소드가 존재한다면 혼동을 방지하여 메소드가 생성되지 않는다.
  • @NonNull

    • 메소드나 생성자에 사용하게 되면 null check를 해준다.
  • @AllArgsConstructor

    • 모든 변수를 사용하는 생성자를 만들어준다.
  • @NoArgsConstructor

    • 파라미터가 없는 기본 생성자를 만들어준다.
  • @RequiredArgsConstructor

    • 특정 변수만을 활용하는 생성자를 만들어준다.
    • 초기화 되지 않은 final 필드나, @NonNull 어노테이션이 붙여진 필드에 대해 생성자를 만들어준다.
  • @EqaulsAndHashCode

    • 클래스에 대한 eqauls 함수와 hashCode 함수를 생성해준다.
    @EqualsAndHashCode(of = {"name", "description"}, callSuper = false)
    public class Example extends Common {
        String name;
        String description;
    }
    • 서로 다른 두 객체에서 특정 변수의 이름이 같은 경우 같은 객체로 판단하고자 할 때 위와 같이 사용한다.
    • callSuper=false는 Common을 상속하는데, 이 상위 클래스는 적용시키지 않을 때 사용한다.
    • .include.Exclude를 활용해서 명시적으로 선택할 수도 있다.
    • 자기 자신을 포함하는 배열을 가지거나 순환 참조가 존재하는 경우 명시적으로 제외하지 않으면 StackOverFlowError가 발생한다.
  • @ToString

    • 클래스 변수들의 ToString() 메소드를 생성해준다.
    • 출력을 원하지 않는 변수는 @ToString.Exclude 어노테이션을 붙여주면 제외할 수 있다.
    • callSuper=true는 마찬가지로 상위클래스에 대해서 적용하고자 할때 사용된다.
    • .include.Exclude를 활용해서 명시적으로 선택할 수도 있다.
  • @Data

    • @Getter, @Setter, @RequiredArgsConstructor, @EqaulsAndHashCode, @ToString 어노테이션을 자동생성한다.
  • @Value

    • @Data의 불변 클래스 버전으로 모든 필드를 private final로 만들고 setter는 생성되지 않는다.
    • 클래스 또한 final로 만든다.
  • @Builder

    • 해당 클래스의 객체 생성에 빌더 패턴을 적용시켜준다.
    • 모든 변수들에 대해 빌더 패턴을 적용하려면 클래스 위에 어노테이션을 붙이고, 특정 변수만 하고 싶은 경우는 원하는 생성자를 생성하고 생성자 위에 어노테이션을 붙여준다.
    • 오직 @NoArgsConstructor와 함께 사용하면 컴파일 에러가 난다. 빌더 패턴을 위한 모든 프로퍼티를 필요로 하기 때문에 @AllArgsConstructor도 반드시 함께 사용해야 한다.
    • 모든 생성자를 직접 만들어도 된다
  • @Delegate

    • 한 객체의 메소드를 다른 객체로 위임한다.
    • 음.. 잘 쓰지 않는 것 같다
  • @Log4j2

    • 해당 클래스의 로그 클래스를 자동완성 시켜준다.
  • @CleanUp

    • close()를 호출해준다.

RestController

전통적인 Spring MVC에서의 @Controller는 주로 View를 반환하기 위한 용도로 사용되었다. MVC 컨테이너는 Client의 요청으로부터 View를 반환한다.

Spring MVC의 @Controller에서 데이터를 반환하기 위해서는 컨트롤러의 각 메소드에 @ResponseBody 어노테이션을 사용하면 JSON 형태로 데이터를 반환할 수 있다. 이 때 ViewResolver를 통해 데이터를 담을 View를 찾는 과정을 거치게 된다.

Spring 4.0에서 Spring 프레임워크에서 RESTful 웹 서비스를 쉽게 개발할 수 있도록 @RestController라는 것이 추가됨.

RestController@Controller@ResponseBody의 조합으로 단순히 객체만을 반환하고 객체 데이터는 XML / JSON 형식으로 HTTP 응답에 담아서 전송된다.

- HttpMessageConverter

스프링 프레임워크에서 제공하는 인터페이스 中

HTTP 요청의 본문을 객체로 변경하건, 객체를 HTTP 응답 본문으로 변경할 때 사용된다.

뷰가 아니라 객체를 응답할 때는 viewResolver 대신에 HttpMessageConverter가 동작하는데, HttpMessageConverter에는 여러 Converter가 등록되어 있고 반환하는 데이터에 따라 사용되는 Converter가 달라진다는 특징이 있다.

리턴 타입이 application/json인 경우에는 MappingJackson2HttpMessageConverter가 사용되고, 클라이언트의 Http Accept 헤더와 서버의 컨트롤러 return type 정보를 종합해 적절한 HttpMessageConverter가 채택된다.

그냥 @Controller를 사용할 때는 @ResponseBody를 넣어줘야 MessageConverter가 적용되고, 선언하지 않으면 BeanNameViewResolver에 의해서 viewName에 해당하는 뷰를 찾으려고 할 것이다.

즉, 클라이언트의 요청이 들어오면 디스패처 서블릿에 의해서 선택된 컨트롤러가 api를 실행하고 이 때 @ResponseBody 어노테이션이 선언되어있으면 Obejct 값을 Body에 넘겨주기 위해서 HttpMessageConverter가 사용된다. 이 때는 반환값에 따라서 각기 다른 Converter가 사용됩니다.

ResponseEntity

먼저 Spring framework에 **HttpEntity**라는 클래스가 존재한다.

이 클래스는 HTTP 요청(request)이나 응답(response)에 해당하는 HttpHeader와 HttpBody를 포함하는 클래스이다.

public class HttpEntity<T> {
    private final HttpHeaders = headers;
    
    @Nullable
    private final T body;
}
public class RequestEntity<T> extends HttpEntity<T>
public class ResponseEntity<T> extends HttpEntity<T>

RequestEntity와 ResponseEntity는 이렇게 HttpEntity 클래스를 상속받아 구현한 클래스이다.

ResponseEntity는 사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스이다. 따라서, HttpStatus, HttpHeaders, HttpBody를 포함한다.

ResponseEntity의 생성자는 this()를 통해서 매개변수가 3개인 생성자로 들어간다.

public ResponseEntity(HttpStatus status) {
    this(null, null, status);
}
public ResponseEntity(@Nullable T body, HttpStatus status) {
    this(body, null, status);
}

또한 상태코드(Status), 헤더(headers), 응답데이터(ResponseData)를 담는 생성자도 존재한다.

public class ResponseEntity<T> extends HttpEntity<T> {
    public ResponseEntity(@Nullabe T body, @Nullable MultiValueMap<String, String> headers,
                         HttpStatus status) {
        super(body, headers);
        Assert.notNull(status, "HttpStatus must not be null");
        this.status = status;
    }
}

클라이언트에게 응답을 보내는 예제를 만들어보자.

@RestController
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserController {
    private final UserService userservice;
    
    @GetMapping("/api/user/{id}")
    public ResponseEntity<Message> findById(@PathVariable int id) {
        User user = userservice.findById(id);
        Message message = new Message();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(new MeidaType("application", "json", Charset.forName("UTF-8")));
        
        message.setStatus(StatusEnum.OK);
        message.setMessage("성공했어요");
        message.setData(user);
        
        return new ResponseEntity<>(message, headers, HttpStatus.OK);
    }
}

REST API 개발 시 ResponseEntity의 값(헤더, 상태코드)들을 적절히 활용해보자.

RestTemplate

  • Spring 3.0부터 지원하는 Spring의 HTTP 통신 템플릿 (server to servet)
  • RESTful의 원칙을 지킬 수 있으며 HTTP 메소드들에 적합한 여러 메소드가 제공됨
  • HTTP 요청 후 JSON, XML, string과 같이 응답을 받을 수 있는 템플릿 (그게 아니라면 직접 라이브러리로 파싱해야함)
  • Blocking I/O 기반의 동기방식을 사용(REST API 호출 후 응답을 받을 때까지 기다린다)
  • Header와 Content-Type을 설정해서 외부 API 호출 가능
  • 단순 메소드 호출 만으로 복잡한 작업을 쉽게 처리 가능

HttpClient : HTTP를 사용해 통신하는 범용 라이브러리로 RestTemplate은 HttpClient를 추상화 해서 제공한다.

다른 HTTP 통신방법으로는 URLConnecton이 있는데 이는 타임아웃을 설정할 수 없고 쿠키를 제어할 수 없다는 단점이 있다. 또 응답 코드가 4xx, 5xx가 되면 IOException이 발생한다.

또 다른 방식으로는 HttpClient가 있는데 모든 응답코드에 대응이 가능하고 타임아웃 설정과 쿠키제어가 가능하지만 여전히 코드의 가독성이 좋지 않으며 스트림 처리를 별도 로직을 작성해야 하고 응답 컨텐츠 타입에 따라서 별도 로직이 필요하다는 단점이 있다.

그래서 이러한 단점들을 보완하는 RestTemplate이 현재 가장 많이 사용된다.

RestTemplate은 HttpClient를 추상화해서 제공이 되어 있어 내부 통신은 Apache HttpComponents를 사용한다.

> RestTemplate 메서드

image-20210924155919175

  • getForObject()

    Product product = restTemplate.getForObject(BASE_URL + "/{id}", Prodcut.class);

    Product로의 매핑은 기본적으로 jackson-databind가 담당한다.

  • getForEntity()

    • 응답을 ResponseEntity 객체로 받는다.
    • getForObject()와 달리 HTTP 응답에 대한 추가 정보를 담고 있어 GET 요청에 대한 응답 코드, 실제 데이터를 확인할 수 있다.
    • 또, ResponseEntity 제네릭 타입으로 응답을 String이나 Object 객체로 유연하게 받는 것이 가능하다.
    ResponseEntity<String> responseEntity = restTemplate.getForEntity(BASE_URL + "/{id}", String.class, 25);
    log.info("statusCode: {}", responseEntity.getStatusCode());
    log.info("getBody: {}", responseEntity.getBody());
    • getForEntity()에 여러 값을 담을 params를 같이 넘겨줄 수 있다. LinkedMultiValueMap 객체에 담아서 parmas로 넘겨줄 수 있다.
    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("name", "book");
    params.add("description", "best-seller");
    
    ResponseEntity<Product> responseEntity = restTemplate.getForEntity(BASE_URL + "/{name}/{description}", Product.class, params);
    log.info("statusCode: {}", responseEntity.getStatusCode());
    log.info("getBody: {}", responseEntity.getBody());
  • get 요청에 header가 필요한 경우

    • get Method에서는 header를 추가할 수가 없으므로 exchange method를 사용해야 한다.
    HttpHeaders headers = new HttpHeaders();
    headers.set("header", header);
    headers.set("header2", header2);
    
    HttpEntity request = new HttpEntity(headers);
    
    ResponseEntity<String> response = restTemplate.exchange(
    	URL_PATH,
        HttpMethod.GET,
        request,
        String.class
    );
  • get 요청에 header 값과 쿼리 스트링(query String, param)이 필요한 경우

    • post처럼 HttpEntity에 넣어서 요청할 수가 없다.
    HttpHeaders headers = new HttpHeaders();
    headers.set("header", header);
    headers.set("header2", header2);
    
    HttpEntity request = new HttpEntity(headers);
    
    UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(URL_PATH).queryParam("keywords", "11");
    
    ResopnseEntity<String> response = resTemplate.exchange(
    	uriBuilder.toUriString(),
        HttpMethod.GET,
        request,
        String.class
    );
    • 이렇게 UriBuilder를 사용해서 넣는 수 밖에 없다.
    • post방식과 달리 httpEntity에 같이 넣거나 exchange의 parameter로 넘길 수 가 없다.
    • 굳이 uriBuilder를 쓰지 않고 map에 파라미터를 추가하고 map을 parameter 문자열로 변환해주는 메소드를 만들어서 사용하는 것이 편할 수 있다.
    HttpHeaders headers = new HttpHeaders();
    headers.set("header", header); 
    headers.set("header2", header2);
    
    HttpEntity request = new HttpEntity(headers);
    
    Map<String, String> params = new HashMap<String, String>();
    params.put("query1", "test1");
    params.put("query2", "test2");
    
    ResponseEntity<String> response = restTemplate.exchange(
    	URL_PATH + "?" + this.mapToUriParam(params),
        HttpMethod.GET,
        request,
        String.class
    );
    
    ...
        
    private static String mapToUriParam(Map<String, Object> params) {
        StringBuffer paramData = new StringBuffer();
        for (Map.Entry<String, Object> param : params.entrySet()) {
            if (paramData.length() != 0) {
                paramData.append("&");
            }
            paramData.append(param.getKey());
            paramData.append("=");
            paramData.append(String.valueOf(param.getValue()));
        }
        return paramData.toString();
    }
  • postForObject() Method에 header 값이 없는 경우

    Product newProduct = Product.builder()
        .name("book")
        .description("best-seller").build();
    
    Product product = restTemplate.postForObject(BASE_URL + "/product", newProduct, Prodcut.class);
  • postForObject() Method에 header 포함해서 보내기

    Prodcut newProduct = Product.builder()
        .name("book")
        .description("best-seller").build();
    
    HttpHeaders headers = new HttpHeaders();
    headers.set("headerTest", "headerValue");
    
    HttpEntity<Product> request = new HttpEntity<newProduct, headers);
    
    Product product = restTemplate.postForObject(BASE_URL + "/product", request, Product.class);
  • post에 form data를 사용

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    
    MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
    
    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
    
    ResponseEntity<String> response = restTemplate.postForEntity(BASE_URL + "/form", request, String.class);
  • TimeOut 설정하기

    • timeOut을 설정하려면 ClientHttpRequestFactory와 같은 팩토리 메소드를 만들고 RestTemplate의 생성자에 추가해야 한다.
    RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
    
    private ClientHttpRequestFactory getClientHttpRequestFactory() {
        int timeout = 5000;
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        clientHttpRequestFactory.setConnetTimeout(timeout);
        
        return clientHttpRequestFactory;
    }
    // timeout = 0 : 무제한 설정
  • Execute()

    • Execute()는 콜백을 통해 요청 준비와 응답 추출을 완벽하게 제어하여 요청을 수행하는 가장 일반적인 메서드를 ResTemplate에서 제공한다.
    • getForObejct()나 postForObject() 등은 execute()를 내부적으로 호출한다.

> RestTemplate의 동작원리

image-20210924160054392

  1. 어플리케이션이 RestTemplate을 생성하고, URI, HTTP 메소드 등의 헤더를 담아 요청한다.
  2. RestTemplate은 HttpMessageConverter를 사용해 requestEntity를 요청 메세지로 변환한다.
  3. RestTemplate은 ClientHttpRequestFactory로부터 ClientHttpRequest를 가져와서 요청을 보낸다.
  4. ClientHttpRequest는 요청 메시지를 만들어서 HTTP 프로토콜을 통해 서버와 통신한다.
  5. restTemplate은 ResponseErrorHandler로 오류를 확인하고 있다면 처리로직을 태운다.
  6. ResponseErrorHandler는 오류가 있다면 ClientHttpResponse 에서 응답데이터를 가져와서 처리한다.
  7. RestTemplate은 HttpMessageConverter를 이용해 응답메세지를 Java Object(Class ResponseType)로 변환한다.
  8. 어플리케이션에 반환한다.

참조

https://velog.io/@soosungp33/%EC%8A%A4%ED%94%84%EB%A7%81-RestTemplate-%EC%A0%95%EB%A6%AC%EC%9A%94%EC%B2%AD-%ED%95%A8

https://juntcom.tistory.com/141

Entity에 관하여

엔티티 클래스는 테이블과 매핑되어 사용되는 클래스이다.

@Entity 어노테이션으로 JPA에게 테이블과 매핑될 클래스이니 관리 할 것임을 알리고

@Table(name = 'product')로 DB와 매핑될 테이블 네임을 지정한다. class name과 table name이 같다면 생략해도 된다.

@Column(name = 'id')도 마찬가지로 DB의 컬럼과 매핑될 필드로 name이 같다면 생략해도 된다.

++ 일반적으로 id를 Long타입의 래퍼클래스로 받는 이유? : long값의 경우 기본값으로 0이 데이터베이스에 들어갈 수 있는데, 기존 데이터베이스에도 id가 0값으로 들어있다면 이게 기존데이터인지 추가된 데이터인지 구분이 어렵다. 따라서, Long타입으로 하면 null로 들어가므로 구분이 쉬워진다.

> Entity의 상태

엔티티 클래스를 바탕으로 생성된 엔티티는 생성부터 소멸까지 총 4가지의 상태를 가지며, 데이터의 입출력 즉 트랜잭션과 연관이 깊다.

  • New / 비영속
  • Managed / 영속
  • Detached / 준영속
  • Removed / 삭제

- NEW / 비영속

엔티티를 생성한 시점부터 트랜잭션 구간에 진입하기 전까지, 엔티티는 비영속 상태이다.

이 상태에서 엔티티는 데이터베이스와 전혀 관계가 없고 JPA의 어떤 특징도 보이지 않는 평범한 객체이다.

Product prodcut = new Product();

- MANAGED / 영속

엔티티가 트랜잭션 영역에 진입해서 엔티티 매니저의 관리 하에 들어가면 해당 트랜잭션 구간동안 엔티티는 영속 상태가 된다.

em.persist(product)

persist() method는 주로 JPARepository에서 save시 일어나게 된다.

엔티티는 이 영속 상태에서 몇 가지 중요한 특징을 가지게 된다.

  • 1차 캐시 사용
  • 같은 키(식별값)를 사용하는 여러 객체의 동일성 보장
  • 쓰기 지연과, 변경감지

이에 대한 자세한 내용은 아래에서 다룬다.

- DETACHED / 준영속

엔티티가 커밋되어 트랜잭션 구간에서 빠져나오는 경우, 이 엔티티는 준영속 상태가 된다.

비영속 상태와 거의 같지만, 영속 상태를 한 번 거쳤기 때문에, 준영속 상태의 엔티티는 식별값을 가지고 있다.

이렇게 한번 DB에 저장된 기존 식별자(ID)를 갖는 엔티티는 영속성 컨텍스트가 더 이상 관리되지 않고, 따라서 변경 감지도 적용되지 않는다.

이러한 준영속 상태의 엔티티를 수정하는 방법은 2가지가 있다.

  1. 변경 감지(dirty checking)
  2. 병합 (merge)

변경 감지 사용

// 변경감지 기능 사용 예시
// 병합기능(merge)의 단점은 엔티티의 모든 속성이 변경되기 때문에 null이 잘못 들어 갈 수가 있다는 것
// 실무는 복잡하기 때문에 가급적이면 병합기능를 쓰지 않고, 조금 귀찮더라도 변경감지 기능을 이용하는 것이 좋다.
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity) {
    // 현재 findItem은 Transaction 영역에 진입했으므로 영속 상태이다. 따라서. @Transactional에 의해서 commit이 되고 JPA에서 flush가 되서, 변경감지가 일어나서 바뀐 값을 업데이트 쿼리를 날려서 업데이트가 된다.
    Item findItem = itemRepository.findOne(itemId);

    findItem.setName(name);
    findItem.setPrice(price);
    findItem.setStockQuantity(stockQuantity);
}

EntityManager로 Entity를 직접 꺼내서 원하는 값을 수정한다.

이렇게 하면 자동으로 dirtyChecking이 일어난다.

위와 같이 단발성으로 업데이트(setMethod())를 하는 것 보다는 의미 있는 method를 만들어서 사용하는 것이 역추적이 좋고 유지보수에 편하다.

ex. findItem.change(price, name, stockQuantity)

병합 사용

// 병합 기능 사용 예시
...
    
private fianl EntityManager em;

public void save(Item item) {
    if (item.getId() == null) {
        em.persist(item);
    } else {
        // 여기서 param으로 넘어온 item은 영속성 컨텍스트가 아니고
        // merge후 반환된 아이가 영속성 컨텍스트이므로 나중에 Item을 쓰려면 쓸려면 얘를 활용해야 한다.
        Item merge = em.merge(item);
    }
}

Item을 저장할 때, id를 직접 설정되지 않았다면 새로 생성하는 것이므로 persist()를, id를 설정된 객체라면 이미 있는 엔티티를 수정하는 것으로 알고 merge()를 호출하는 식으로 사용된다.

여기서는 준영속 상태의 item 자체를 영속성 컨텍스트에 넣는 것이 아니라, 준영속 객체의 식별자와 일치하는 엔티티를 DB에서 가져와 값을 대입받고, 그에 해당 하는 엔티티를 반환하는 것이다.

또, merge()는 엔티티의 모든 필드를 그대로 변경하기 때문에 item의 name만 변경하고자 셋팅후 merge()한다면 나머지 필드는 기존 값을 잃어버리고 null값이 대입된다.

따라서 실무에서는 가급적 em.find()로 엔티티를 가져와서 직접 값을 수정하는 변경감지 기법을 이용하는 것이 권장된다.

- REMOVED / 삭제

엔티티가 트랜잭션 구간 내에서 관련 메서드에 의해 삭제되는 경우, 매핑되는 데이터의 삭제와 함께 엔티티 또한 삭제 상태가 된다.

객체는 사용 가능한 상태이지만, 재활용 하지 않는 편이 좋다.

> Entity Manager

엔티티의 저장, 수정, 조회, 삭제 와 같이 엔티티를 관리하는 객체이다.

Thread-Safe한 구조이다.

매니저의 책임이 전부 영속성 엔티티의 CRUD와 관련이 있다.

엔티티 매니저는 영속성 상태의 엔티티 관리를 위해서 DB 세션과 밀접한 연관을 가지고 있기 때문에, 하나의 엔티티를 여러 스레드에서 공유하여 사용하면 위험하다.

Thread-Safe한 엔티티 매니저 팩토리를 공유해 각 스레드에서 엔티티 매니저를 생성하는 방식이 권장된다.

  • Entity Manager Factory로부터 Entity Manager를 생성
  • Entity Manager는 DB의 Connection Pool로부터 커넥션 획득
  • 획득한 Connection을 통해서 엔티티의 CRUD를 관리

> Entity Manager와 영속성 컨텍스트

Entity Manager에 의해 관리되는 영속성(MANAGED) 상태의 엔티티는 고유한 식별값(ID)로 구분되어 관리된다.

즉, 영속성 상태에 있는 모든 엔티티는 식별값을 가지고 있어야 한다.

영속성 상태에 있는 엔티티는 아래의 특징을 가진다.

- 1차 캐시 / 엔티티의 동일성 보장

  • 영속성 컨텍스트는 내부에 캐시를 갖고 있고, (id, instatnce)map 형태로 Entity가 저장된다.
  • 데이터베이스에 읽은 이력이 있는 데이터는 이 1차 캐시에 저장되어 재사용된다.
  • 트랜잭션 단위의 굉장히 짧은 메모리 공간이다.

image-20210925174709866

  • em.persist(member)로 member가 영속성 컨텍스트에 영속되면, 1차 캐시는 이를 담는다.
  • 이후에 조회 시에 DB에 접근해서 member1을 찾는 것이 아니라, 1차 캐시를 먼저 훑어서 member1을 바로 찾을 수 있고, 캐시에 없다면 DB에서 검색 후 해당 객체를 1차 캐시에 저장하고 반환한다.
  • 이렇게 1차캐시를 거친 조회로 엔티티의 동일성이 보장되는 것이다.

- 쓰기 지연

transaction.begin();

em.persist(member1);
em.persist(member2);

// -------1-------
em.flush();
// -------2-------

transaction.commit();
  • 1의 영역에서 member insert query를 바로 보내는 것이 아니라, 해당 쿼리를 바로 전송할 수도, 원하는 시점으로 지연시킬 수도 있다.

  • 이는 쓰기 지연 SQL 버퍼에 쿼리를 담아뒀다가, 영속성 컨텍스트의 명령(트랜잭션이 종료되는 시점)에 따라 쿼리들이 DB에 전송되기 때문이다.

    1. 그 과정은, member1이 컨텍스트에 영속되면, 우선 1차 캐시로 들어가고 쿼리는 SQL 버퍼에 들어간다.

    2. member2가 따라서 영속되면 마찬가지로 1차 캐시로 들어가고 쿼리는 SQL 버퍼에 들어간다.

      image-20210925183024178

    3. 이후에, transaction이 commit() 메소드 호출 시 컨텍스트에 버퍼를 비우도록 명령하는 flush()메소드를 호출하면서 그 때 버퍼에 있던 쿼리들이 DB에 넘어가서 쿼리를 반영한다.

      image-20210925183037506

- 지연 로딩

JPA에서 테이블 간의 연관관계는 객체간의 참조를 통해 이루어진다.

서비스가 커질수록 참조하는 개체가 많아지고, 객체가 가지는 데이터의 양이 많아지게 된다.

이렇게 객체가 커질수록 DB로부터 참조하는 객체들의 데이터까지 한꺼번에 가져오는 행동은 부담이 되기 때문에, JPA는 참조하는 객체들의 데이터를 가져오는 시점을 정할 수가 있는데 이를 Fetch Type이라고 한다.

Fetch Type 에는 두 가지가 있다.

  • EAGER
    • 성실한, 열심인
    • 말 그대로 데이터를 가져오는 데 성실하기 때문에 하나의 객체를 DB로부터 참조 객체들의 데이터까지 전부 읽어오는 방식이다.
  • LAZY
    • 게으른
    • 말 그대로 참조 객체들의 데이터들은 무시하고 해당 엔티티의 데이터만을 가져온다.

테이블 설계가 복잡해질 수록 하나의 엔티티가 참조하는 테이블들은 점점 증가하고, 그에 따른 쿼리문도 굉장히 길어진다.

이렇게 복잡한 쿼리문을 본 개발자들은 해당 도메인이 어떻게 설계되었는지 확인해봐야 하고, 논리적인 레이어의 분리가 어렵게 된다.

따라서 EAGER 타입은 유지보수를 힘들게 할 수 있으며, 예상치 못한 SQL이 발생할 수 있다.

지연로딩은 엔티티 조회 시점이 아닌 엔티티 내 연관관계를 참조할 때 해당 연관관계에 대한 SQL이 질의되는 기능이며 fetch = FetchType.LAZY 옵션으로 설정한다.

엔티티 조회 시 연관관계 필드는 프록시 객체로 제공된다.

Member member = EntityManagter.find(Member.class, 1L);

member.getTeam(); // 프록시 객체 초기화 X

member.getTeam().getclass(); // 프록시 객체

member.getTeam().getName(); // 프록시 객체 초기화 및 SQL 질의

위 코드와 같이 지연로딩 되는 연관관계를 참조하기 전까지는 프록시 객체가 초기화되지 않고, 프록시 객체를 참조할 때 프록시 객체가 초기화 되고 SQL이 질의된다.

LAZY 옵션을 사용해 엔티티와 관련 있는 데이터의 로드를 해당 데이터가 필요한 시점까지 지연시킬 수 있다는 장점이 있다.

- 변경감지

  • 트랜잭션 종료 시점에 Entity Manager는 엔티티의 변경사항을 감지해 데이터 베이스에 업데이트 한다.
  • SQL이 flush 되기 전에 SQL 버퍼에 저장될 당시의 객체값(스냅샷 값)과 엔티티의 현재 값을 비교해서 수정이 있을 경우 SQL을 일괄 변경하여 DB에 업데이트 한다.

참조

https://awayday.github.io/2017-04-30/jpa-and-entity/

https://ecsimsw.tistory.com/entry/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-1%EC%B0%A8-%EC%BA%90%EC%8B%9C-%EC%93%B0%EA%B8%B0-%EC%A7%80%EC%97%B0

JpaRepository

Spring-Data-Jpa에서는 반복되는 코드없이 쉽게 JPA Repository를 만들 수 있다. extends JpaRespository<Product, Long>으로 인터페이스를 상속하고 커스텀이 필요한 메소드는 오버라이딩하면 된다.

스프링의 변경감지는 EntityManger별로 수행한다.

같은 쓰레드에서 Spring-Data가 제공하는 Repository들은 하나의 EntityManager를 공유한다. 그래서 하나의 컨테이너에서 여러 Repository가 사용하는 EntityManager는 동일하다.

트랜잭션 커밋은 어디에서 일어날까?

레파지토리를 만들 때 Spring-Data-JpaJpaRepository 인터페이스를 상속하였는데, 스프링 데이터에서 기본 구현체를 제공해주기 때문이다.

Spring-Data-Jpa에서 제공하는 JpaRepository의 기본 구현체는 SimpleJpaRepository이다.

(CrudRepository<>는 단순히 인터페이스이다.)

SimpleJpaRepositorysave()메소드에는 스프링 **@Transactional**이 붙어있으므로 해당 클래스에 있는 수많은 메소드에 트랜잭션이 걸리게 되고, 메소드 성공적으로 return하게 되면 commit도 이루어지게 되는것이다.

@Transactional
public <S extends T> S save(S entity) {
    Assert.notNull(entity, "Entity must not be null.");
    if (this.entityInformation.isNew(entity)) {
        this.em.persist(entity);
        return entity;
    } else {
        return this.em.merge(entity);
    }
}

따라서, save() 메소드와 같이 CRUD method()를 수행하는 시점이 트랜잭션의 시작 과 종료 커밋 시점임을 알 수 있다.

AOP를 활용한 REST API의 Error Handling

SpringBoot에서 기본적으로 오류처리에 대한 동작 흐름에 대해 알아보자.

SpringBoot는 모든 오류를 적절한 방식으로 처리하며 /error로 매핑하는 전역 오류 페이지 등록을 제공한다.

또, http 상태와 예외에 대한 메시지를 JSON으로 응답하거나 html 형식으로 렌더링 하는 whitelabel 페이지 뷰를 제공한다.

BasicErrorController

SpringBoot의 기본 오류 처리

SpringBoot는 오류가 발생하면 server.error.path에 설정된 경로에서 요청을 처리하게 된다.

기본적으로 BasicErrorController가 등록이 되어 있어서 해당 요청을 처리하게 된다.

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}") // 1)
public class BasicErrorController extends AbstractErrorController {

  @Override
  public String getErrorPath() {
    return this.errorProperties.getPath();
  }

  @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // 2)
  public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
            
    HttpStatus status = getStatus(request);
    Map<String, Object> model =
      getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
		
    response.setStatus(status.value());
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
  }

  @RequestMapping // 3)
  public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    
    // 4)
    Map<String, Object> body =
      getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
    HttpStatus status = getStatus(request);
    return new ResponseEntity<>(body, status);
  }    
}
  • 1 : Spring 환경 내에 server.error.path 혹은 error.path로 등록된 property의 값을 넣거나, 없는 경우 /error를 사용
  • 2 : HTML로 응답을 주는경우 errorHtml에서 응답을 처리
  • 3 : HTML 외 응답이 필요한 경우 error에서 처리
  • 4 : 실직적으로 view에 보낼 모델을 생성

따라서 BasicErrorController에서는 HTML 요청, 그 외의 요청을 나누어서 처리할 핸들러를 등록하고 getErrorAttributes를 통해 응답을 위한 모델을 생성한다.

AbstractErrorController

getErrorAttributesBasicErrorController의 상위 클래스인 AbstractErrorController에 구현되어 있다.

public abstract class AbstractErrorController implements ErrorController {
    private final ErrorAttributes errorAttributes;
    
    protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
        WebRequest webRequest = new ServletWebRequest(request);
        return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
    }
}

ErrorAttributes 인터페이스의 getErrorAttributes를 호출한다. (위임자 패턴)

별도로 ErrorAttributes를 등록하지 않았다면 Spring Boot는 DefaultErrorAttributes를 사용한다.

public interface ErrorAttributes {
    // 요청을 기반으로 모델 생성
    Map<String, Obejct> getErrorAttributes(WebRequest webRequest, boolean includeStactTrace);
    // 요청에서 Throwable 획득
    Throwable getError(WebRequest webRequest);
}

public DefaultErrorAttributes {
    // 생성자 및 메서드
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest request, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap<>();
        errorAttributes.put("timestamp", new Date()); // timestamp 생성
        addStatus(errorAttributes, request); // status 생성
        addErrorDetails(errorAttributes, request, includeStackTrace); // 오류 상세 내용 생성
        addPath(errorAttributes, request); // path 생성
        return errorAttributes;
    }
}

ErrorAttributes에서 가져온 모델로 Response를 생성한다.

{
    "timestamp": "2021-09-25T04:24:11.447+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/mypath"
}

ErrorAttributes

여기서 ErrorAttributes는 오류가 발생했을 때 응답을 내려줄 모델을 생성하는데 여기서 우리는 이 ErrorAttributes 인터페이스를 마음껏 구현할 수 가 있다. (Spring에서 제공하는 확장 포인트이다!)

개발자가 ErrorAttributes를 별도로 구현하여 bean으로 등록하면 BasicErrorController는 해당 ErrorAttributes를 사용한다.

임의로 모델에 "greeting" : "HelloWorld"를 추가한 예제이다.

@Component
public class CustromErrorAttributes extends DefaultErrorAttributes {
    
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> result = super.getErrorAttributes(webRequest, includeStackTrace);
        result.put("greeting", "HelloWorld");
        return result;
    }
}
{
    "timestamp": "2021-09-25T04:24:11.447+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/mypath",
    "greeting": "HelloWorld"
}

ErrorAttributes와 마찬가지로 ErrorController의 구현체를 개발자가 직접 bean으로 등록한다면, SpringBoot는 해당 Bean을 먼저 찾아서 BasicErrorController 대신 오류 처리를 위해 사용하게 된다.

위임자 패턴을 사용해서 기본적인 처리는 BasicErrorController에게 위임하고, 나머지 필요한 처리를 추가할 수 있다.

@Slf4j
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class CustomErrorController extends BasicErrorController {

    public CustomErrorController(ErrorAttributes errorAttributes,
                                 ServerProperties serverProperties,
                                 List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, serverProperties.getError(), errorViewResolvers);
    }

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request,
                                  HttpServletResponse response) {
        log(request); // 로그 추가
        return super.errorHtml(request, response);
    }

    @Override
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        log(request);
        return super.error(request);
    }

    private void log(HttpServletRequest request) {
        log.error("error");
    }
}

> ErrorController의 호출 흐름

  1. 서블릿 컨테이너(ex. tomcat)에서 등록된 서블릿에서 요청을 처리하다가
  2. 오류가 발생해서
  3. 해당 서블릿에서 처리하지 못하고 서블릿 컨테이너까지 오류가 전파 되었을때 (SevletException으로 래핑된다)
  4. 서블릿 컨테이너가 오류를 처리하기 위해 특정 경로(server.error.path)로 해당 요청처리를 위임 (ErrorController를 호출한다)

@ExceptionHandler

스프링에서는 발생한 Exception을 기반으로 오류를 처리할 수 있도록 @ExceptionHandler를 제공한다.

@RestController
@RequestMapping("/products") {
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(ProductNotFoundException.class)
    pubilc Map<String, String> handle(ProductNotFoundException e) {
        log.error(e.getMessage(), e);
        Map<String, String> errorAttributes = new HashMap<>();
        errorAttributes.put("code", "PRODUCT_NOT_FOUND");
        errorAttributes.put("message", e.getMessage());
        return errorAttributes;
    }
}

특정 컨트롤러에서 예외가 발생한 경우, Spring은 @ExceptionHandler를 검색해서 해당 Annotation에 선언된 예외 및 하위 예외에 대해서 특정 메서드가 처리할 수 있도록 한다.

보통의 핸들러와 마찬가지로 ModelAndViewString을 반환해 View를 Resolve할 수 있고, ResponseEntity<T>를 반환할 수도 있다.

@ControllerAdvice

Spring에서는 Bean으로 등록되는 @Controller들을 선택적으로, 혹은 전역으로 공통 설정을 적용할 수 있는 @ControllerAdvice를 사용할 수 있다.

@ControllerAdvice에서 사용할 수 있는 것 중 하나가 @ExceptionHandler이다.

@Slf4j
@ControllerAdvice
public class GlobalControllerAdvice {
    
    @ReponseStatus(HttpSTatus.NOT_FOUND)
    @ExceptionHandler(ProductNotFoundException.class)
    public Object handle(ProdcutNotFoundException e, HttpServletRequest request) {
        if (...) {
            return makeJson(e);
        } else {
            return "/error/404";
        }
    }
}

위의 예제는 하나의 method에서 JSON응답과 HTML 응답을 나누었는데,

HTML view를 사용할 경우와 JSON view를 사용할 경우를 나누어서 ControllerAdvice를 등록하고 @Order를 사용해 우선순위를 부여하면 분기처리 없이 나누어서 오류를 처리할 수 있다.

@Slf4j
@Order(ORDER)
@RestControllerAdvice(annotations = RestController.class)
public class GlobalRestControllerAdvice {
    public static final int ORDER = 0;
    
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(ProductNotFoundException.class)
    public Map<String, String> handle(ProductNotFoundException e) {
        log.error(e.getMessage(), e);
        Map<String, String> errorAttributes = new HashMap<>();
        errorAttributes.put("code", "BOARD_NOT_FOUND");
        errorAttributes.put("message", e.getMessage());
        return errorAttributes;
    }
}

@Slf4j
@Order(GlobalRestControllerAdvice.ORDER + 1)
@ControllerAdvice
public class GlobalHtmlControllerAdvice {

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(ProductNotFoundException.class)
    public String handle(ProductNotFoundException e, Model model, HttpServletRequest request) {
        log.error(e.getMessage(), e);
        model.addAttribute("timestamp", LocalDateTime.now());
        model.addAttribute("error", "BOARD_NOT_FOUND");
        model.addAttribute("path", request.getRequestURI());
        model.addAttribute("message", e.getMessage());
        return "/error/404";
    }
}

ResponseEntityExceptionHandler

ControllerAdvice를 사용해서 Exception처리를 한 곳으로 모으는 경우에는 ResponseEntityExceptionHandler를 상속받아서 Spring MVC에서 기본으로 제공되는 Exception들의 처리를 간단하게 등록할 수 있다.

갹 Exception 처리를 위한 메소드들은 모두 protected로 선언되어 있으며 하위 클래스에서 필요에 따라 Override할 수 있다.

public abstract class ResponseEntityExceptionHandler {
  @ExceptionHandler({
    HttpRequestMethodNotSupportedException.class, // 405
    HttpMediaTypeNotSupportedException.class, // 415
    HttpMediaTypeNotAcceptableException.class, // 406
    MissingPathVariableException.class, // 500
    MissingServletRequestParameterException.class, // 400
    ServletRequestBindingException.class, // 400
    ConversionNotSupportedException.class, // 500
    TypeMismatchException.class, // 400
    HttpMessageNotReadableException.class, // 400
    HttpMessageNotWritableException.class, // 500
    MethodArgumentNotValidException.class, // 400
    MissingServletRequestPartException.class, // 400
    BindException.class,
    NoHandlerFoundException.class, // 404
    AsyncRequestTimeoutException.class // 503
  })
  @Nullable
  public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
    // 각 예외에 대한 분기처리 로직(상속 구현 가능하도록 protected로 메서드가 선언되어 있음)
  }
  
  // 각 예외 처리를 위한 protected 메서드들이 있음
  protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
      HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
    // 예외 처리
  }
}

이를 활용해서 실제 적용한 예제를 보자.

@RestController
@ControllerAdvice
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(Exception.class)
    public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ExceptionResponse.builder()
                .timestamp(new Date())
                .message(ex.getMessage())
                .details(request.getDescription(false)).build());

    }

    @ExceptionHandler(ProductNotFoundException.class)
    public final ResponseEntity<Object> handleProductNotFoundExceptions(Exception ex, WebRequest request) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ExceptionResponse.builder()
                .timestamp(new Date())
                .message(ex.getMessage())
                .details(request.getDescription(false)).build());

    }
}

@ExceptionHandler의 인자로 ~~Exception.class를 정의하여 Exception의 대상을 다양하게 처리할 수 있다.

SpringBoot가 제공하는 ErrorAttributes는 단일 구현으로 에러를 처리하기 때문에 모듈별로 Exception을 상속해서 별도 정의하는 경우에는 다양한 에러에 대응을 하나의 구현으로 처리하는 것은 무리가 있다.

따라서 다양한 Exception에 대해 별도 정의가 가능한 Global Exception Handler 방식을 선호한다.

그 이외에도 HandlerExceptionResolver 인터페이스를 사용해서 요청, 응답, 핸들러, 예외를 파라미터로 받아서 ModelAndView를 반환값으로 하는 resolveException 메소드가 있는데, 이는 추후에 다시 알아보도록 할 예정이다.

Filter와 Interceptor

Filter와 Intercepter는 실행되는 위치가 다르므로 Exception이 발생했을 때 처리하는 방법도 달라진다.

Interceptor는 DispatcherServlet 내부에서 발생하기 때문에 ContollerAdvice를 적용할 수 있지만 Filter는 DispatcherServlet 외부에서 발생하기 때문에 ErrorController에서 처리해야 한다.

image-20210925045804677

참조 : https://supawer0728.github.io/2019/04/04/spring-error-handling/

Hibernate의 기본키 생성전략

DDD: 도메인주도설계

객체지향의 핵심은 무엇일까?

객체지향에서의 핵심은 실세계의 객체(물건, 사람, 주문, .. 주도적으로 뭔가 생산할 수 있는 주체) 들이 서로간의 상호작용을 바탕으로 책임,협력,역할의 관점을 가지고 메세지를 교환하는 것이다.

객체지향의 핵심은 결국 **객체(무언가를 만드는 주체)**라고 할 수 있다.

그렇다면 어떤 객체가 필요한지 알 수 있고 어떻게 이 객체들을 추려내서 상호작용할 수 있을까?

이를 해결할 수 있는 것이 바로 **도메인 주도 설계(DDD: Domain Driven Design)**이다.

다시 말해서, 도메인을 중심으로 설계해 나가는 것이다.

도메인이란 실세계에서 사건이 발생하는 집합이라고 생각하면 쉽다.

쇼핑몰을 예로 들면,

쇼핑몰에서는 손님이 주문하는 도메인(Order Domain)이 있을 수 있고,

점주입장에서 옷들을 관리하는 도메인(Manage Domain)이 있을 수 있고

쇼핑몰 입장에서 월세, 관리비 등 건물에 대한 관리를 담당하는 도메인(Building Domain)이 있을 수 있다.

이러한 여러가지 도메인들이 서로 상호작용 하며, 설계해 나가는 것이 도메인 주도 설계이다.

DDD의 특징은 같은 객체(Object or Class)가 여러 개가 존재할 수 있다는 것이다.

주문 도메인에서 옷은 손님들에게 팔기 위한 객체 정보(name, price, ...)를 담고 있지만,

옷을 관리하는 도메인에서는 옷은 점주 집장에서 관리하기 위한 객체정보(madeTime, size, madeFrom, ...)를 위주로 담고 있는다.

다시 말해서 문맥(Context)에 따라 객체(Object)의 역할이 바뀐다는 것이다.

image-20211007102443192

Bounded Context는 Domain-Driven-Design의 중심 패턴이다. 대규모 모델과 팀을 다루는 것이 DDD의 설계 초점이다.

DDD는 큰 모델을 서로 다른 Bounded Context로 나누고 상호관계를 명시하여 처리한다.

Bounded Context에 따라서 모델들의 역할은 완벽히 달라지고, 책임 또한 달라지게 된다.

이는 Aggregate의 명시적 경계가 된다.

그래서 이를 외부로(public) 노출시키지 않고, package-private하게 내부에서만 알 수 있게 해야 하는 것이다. 이러한 관점을 더 나아가 직접 서비스에 적용한 것이 바로 MircorService이다.

다시 말해서 서로 다른 도메인 영역에 영향을 끼치기 위해서는 API로 호출을 해야 하는 것이다.

즉, 각각의 도메인은 서로 철저히 분리되고, 높은 응집력과 낮은 결합도로 변경과 확장에 용이한 설계를 얻게 될 수 있다.

그러면 어떻게 도메인 주도 설계를 구현할까?

image-20211007104326258

크게 3가지 Layer로 구분하는 것이 핵심이다.

  1. Application Layer
    • 주로 도메인과 Repository를 바탕으로 실제 서비스(API)를 제공하는 계층
    • 다른 시스템의 애플리케이션 계층과 상호작용 하기 위해 필요한 계층
    • 따라서 비즈니스 규칙이나 지식은 포함하지 않고 로직을 직접 수행하기보다는 도메일 모델에 로직 수행을 위임하는 역할이다.
    • 트랜잭션을 처리하고, 도메인 영역에서 발생시킨 이벤트를 처리하는 역할을 한다.
  2. Domain Layer
    • Entity, VO를 활용해 도메인 로직이 진행되는 계층
    • 이 계층이 도메인의 비즈니스 로직의 핵심이 되는 계층이다.
  3. Infrastructure Layer
    • 엔티티에 있던 데이터가 데이터베이스나 기타 영구 저장소에 유지되는 방식으로 외부와의 통신(ORM, DB) 및 외부 기술, 라이브러리를 담당하는 계층
    • 도메인 계층을 오염시키지 않아야 하고 프레임워크에 대한 고정 종속성을 지양함으로써 도메인 모델 엔티티 클래스는 데이터 유지에 사용되는 인프라와는 무관하게 유지해야 한다.

여기서 맨 앞단에 추가로 사용자에게 화면을 보여주고 세션을 관리하는 Presentation Layer(Controller)가 추가되기도 한다.

각각의 도메인들을 위와 같은 Layer로 철저하게 분리해서 만드는 것이 DDD의 핵심 설계 방식이다.

이렇게 설계한 도메인들을 모듈(Module)별로 분리하는 것이 마이크로서비스(MicroService).

핵심은 결국 도메인을 서비스 별로 분리하는 것!

하지만 모든 도메인에서 많은 객체(Object or Class)들을 다루고 있다면, 유지보수나 확장에 어려움이 있을 수 밖에 없다.

객체간의 상호작용은 점점 복잡해지고, 어떤 객체가 도메인을 대표하는 객체인지 혼란이 오기 때문이다.

그렇다고 엔티티 객체마다 Repository를 만드는 것은 마이크로 서비스의 장점이 희석된다.

여기서 등장하는 것이 바로 **집합(Aggregate)**이다.

image-20211007105622950

쉽게 얘기하면, 각각의 도메인 영역을 대표하는 객체가 바로 Aggregate이다.

이렇게 집합으로 나누게 되면, 각각의 도메인에 Repository로 묶어야 하는 객체(Entity)가 명확해질 수 있다.

Top-Down 방식으로 계층을 타고타고 내려갈 때 어떤 Entity를 만들기 위해 도메인이 이루어져 있는지 명확하게 들어오기도 한다.

위 예시에서는 Order라는 Domain에서 Order라는 Entity를 만들기 위해 다양한 객체들(Object or Class)이 상호 협력하고 있는 관계로 볼 수 있다.

이 Aggregate를 명확하고 잘 설계하는 것이 핵심이며 중요한 과제이다.

참조

https://martinfowler.com/bliki/BoundedContext.html

https://huisam.tistory.com/entry/DDD

JPA IN Clause

Spring JPA Query에서 사용할 수 있는 IN 절에 대해서 살펴본다.

ref : https://javadeveloperzone.com/spring/spring-jpa-query-in-clause-example/

image-20211001144219815

id가 1 또는 2 또는 3인 employee에 대해 select 하는 쿼리이다.

우리는 JPA를 사용하기 위해 ListCollection으로의 변환이 필요한데, Spring Data JPA는 IN 쿼리를 지원하는 기본 쿼리를 제공하고 있다.

그러면 실제로 어떻게 사용하는지 예시와 함께 보자.

image-20211001144843967

  1. 첫 번째 방법은 메서드 이름으로 레코드를 가져오는 것이다. 예약된 이름 규칙으로 메서드를 정의하게 되면 Spring JPA가 런타임시에 자동으로 쿼리를 생성하고 결과를 반환한다.

  2. 두 번째 방법은 매개변수로 @Query 를 사용해서 List타입의 List<String> names를 Parameter로 넘겨준다.

    이 때 @Param("names")로 들어오는 매개변수가 어떤 컬럼과 매칭될 것인지 결정한다.

    이 어노테이션이 생략된다면 자동으로 매개변수 이름인 names라는 컬럼을 이름으로 매핑된다.

  3. 세 번째 방법은 native 쿼리는 이용하는 방법이고, nativeQuery = true가 의미하는 것은 value 값에 native query를 포함하고 있다는 뜻.

Output

image-20211001145611077

Query

image-20211001145636462

ModelMapper

엔티티와 DTO간에 변환 시 자동으로 Object를 매핑시켜주는 라이브러리

주의: 매핑해줄 클래스에는 setter가 있어야하고 매핑이 되는 클래스에는 getter가 있어야 사용 가능하다!

기본적으로 ModelMapper에서 제공하는 map() 메서드를 이용하면 변환할 수 있고 클래스 내부에 있는 변수들의 이름을 분석해서 자동 매핑시켜주는 방식이다.

map() 메소드가 호출되면 source(from)와 destination(to)의 타입이 분석되고 matching strategyconfiguration에 의해서 어느 프로퍼티가 매칭될지 결정된다.

때에 따라서 매핑을 명시적으로 정의해야 하는데, ModelMapper는 다양한 매핑 접근 방식을 지원하므로 메서드와 필드참조를 혼합해서 사용할 수 있다.

modelMapper.typeMap(Order.class, Order.DTO.class).addMappings(mapper -> {
    mapper.map(src -> src.getBillingAddress().getStreet(),
              Destination::setBillingStreet);
    mapper.map(src -> src.getBillingAddress().getCity(),
              Destination::setBillingCity);
});

Modelmapper에는 대상 속성이 일치하지 않는 경우 알려주는 기본 유효성 검사가 있다.

먼저 다음과 같이 ModelMapper에 검증하려는 유형을 알려야 한다.

modelMapper.createTypeMap(Order.class, OrderDTO.clss);

혹은, 소스 및 대상 타입이 아직 존재하지 않는 경우라면 **PropertyMap**의 자동생성으로 매핑을 추가할 수 있다.

modelMapper.addMappings(new OrderMap());

그런 다음 매핑의 유효성을 검사할 수 있다.

modelMapper.validate();

만약 소스타입과 대상타입의 어느하나라도 일치하지 않으면 ValidationException을 발생하게 된다.

기본적으로 매칭전략에 맞지 않는 속성들은 null값으로 초기화 하게 된다. 개발자는 어떤 객체에 대해 정상적으로 매핑되었는지 검증할 필요가 있는 것이다.

// Example
modelMapper = new ModelMapper();
Order order = modelMapper.map(test, Order.class);
try {
    modelMapper.validate();
} catch (ValidationException e) {
    /* do ExceptoinHandling ! */
}

Property Mapping

프로퍼티와 클래스의 이름이 다른 경우 source와 destination 간에 매핑할 수 있는 PropertyMap을 생성할 수 있도록 지원한다.

source의 getter와 estination의 setter 메서드를 활용해서 프로퍼티를 정의할 수 있다.

typeMap.addMapping(Source::getFirstName, Destination::setName);

또한, source와 destination의 타입이 일치하지 않아도 된다.

typeMap.addMapping(Source::getAge, Destination::setAgeString);

아래 예제는 destination 타입의 setAge 메소드를 souce 타입의 getCustomer().getAge()메서드 계층으로 매핑한다. 이것은 source와 destination 사이에서 deep mapping이 발생하도록 한다.

typeMap.addMaping(src -> src.getCustomer().getAge(), PersonDTO::setAge);

아래는 destination 타입의 getCustomer().setName() 계층 구조를 source 타입의 person.getFirstName()계층 구조로 매핑한다.

typeMap.addMapping(src -> src.getPerson().getFirstName(), (dest, v) -> dest.getCustomer().setName(v));

Matching Strategies

프로퍼티 매칭 전략은 source와 destination간에 매칭 프로세스가 진행되는 동안 매칭 전략이 사용된다.

> Standard

Standard 매칭전략은 모든 대상 속성이 일치하고 모든 소스의 속성 이름에 일치하는 토큰이 하나 이상 있어야 한다.

  • token은 어떤 순서로든 일치될 수 있고.
  • 모든 대상 property name의 token은 반드시 일치해야 하고
  • 모든 소스 property name에은 매칭되는 token이 하나 이상 있어야 한다.

> Loose

Loose 매칭전략은 계층 구조의 마지막 destination 프로퍼티만 일치하도록 요구하며, source 프로퍼티를 destination 프로퍼티와 느슨하게 매칭할 수 있다.

  • token은 어떤 순서로든 일치될 수 있고
  • 마지막 대상 property name에는 모든 token이 일치해야 하고
  • 마지막 소스의 property name에는 매칭되는 token이 하나 이상 있어야 한다.

> Strict

Strict 매칭전략은 말그대로 엄격하게 전략이 적용된다.

이 전략은 불일치나 모호성이 발생하지 않고 완벽한 일치 정확도를 얻을 수 있다.

하지만 source 및 destination 측의 property name token이 반드시 서로 정확하게 일치해야 한다.

  • token은 엄격한 순서로 일치된다.
  • 모든 대상 property name token이 일치해야 하고
  • 모든 소스 property name에는 모든 token이 일치해야 한다.

STRICT전략은 TypeMap을 사용하지 않고도 모호함이나 예기치 않은 매핑이 발생하지 않도록 하는 경우에 사용할 수 있다. 하지만 너무 엄격해서 일부 대상 속성이 일치하지 않는 상태로 남을 수 있다.