네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'web'에 해당되는 글 82건


반응형

 이전 글 

Spring3 RestTemplate이란?

 

[Spring] RestTemplate / spring 에서 http 통신하는 법 / API 호출

🐝 Rest 서비스를 호출하는 방법 - RestTemplate Spring 3부터 지원, REST API 호출 이후 응답을 받을 때까지 기다리는 동기 방식 - AsyncRestTemplate Spring 4에 추가된 비동기 RestTemplate - WebClient Spring 5에 추가된

frogand.tistory.com

 

RestTemplate

Spring에서 지원하는 HTTP를 유용하게 쓸 수 있는 동기식 템플릿 메서드 API이다.

비동기 non-blocking HTTP Client가 필요하다면 WebClient를 사용하자.
WebClient는 동기 및 비동기, 스트리밍 시나리오를 모두 지원한다.
비동기를 지원하는 AsyncRestTemplate는 스프링 5.0부터 deprecated 되었다.
Spring 3.0에서는 RestTemplate
Spring 5.0에서는 WebFlux의 WebClient
Spring 6.0에서는 HTTP Interface

spring-boot-starter-web 라이브러리가 추가되어 있으면 자동으로 하위 항목으로 사용할 수 있다.

 

RestTemplate는 Thread Safe하다.

그래서 흔히들 아래와 같이 @Bean으로 사용하여 자원을 아끼는 구조로 설계한다. 

@Configuration
public class RestConfig {
    @Bean
    RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

이렇게 선언하면 @Autowired를 통해 하나의 RestTemplate으로 HTTP 통신을 처리할 수 있다.

 

RestTemplate의 HTTP 프로토콜 요청

RestTemplate의 동작원리를 살펴보면 기본적으로 HTTP 프로토콜의 요청을 보낼 때 SimpleClientHttpRequestFactory를 활용하여 처리한다.

 

이때, SimpleClientHttpRequestFactory에서 고려해야 할 점이 2가지가 있다.

1. Timeout

SimpleClientHttpRequestFactory의 기본 Timeout 시간은 다음과 같이 설정되어 있다.

private int connectionTimeout = -1;
private int readTimeout = -1;

그래서 Timeout 설정을 하지 않으면 무한정으로 connection을 물고 있는 상태가 발생하여 시스템 오류를 불러 일으킬 수 있다. 

이를 방지하기 위해 일반적으로 아래와 같이 Timeout을 지정하여 일정 시간이 지나도 응답이 없다면 연결을 강제로 끊어주도록 반드시 타임아웃 설정을 해주어야 한다.

@Configuration
public class RestConfig {
    @Bean
    public RestTemplate restTemplate() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnection(5000); // 5초
        factory.setReadTimeout(5000); // 5초

        return new RestTemplate(factory);
    }
}

스프링 부트를 이용하는 상황이라면 RestTemplateBuilder가 자동 구성에 의해 등록되므로 다음과 같이 설정할 수 있다.

@Configuration
public class RestConfig {
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder.setConnectTimeout(Duration.ofSeconds(5))
                                  .setReadTimeout(Duration.ofSecondes(5))
                                  .build();
    }
}

2. Connection 방식

RestTemplate는 기본적으로 Connection Pool을 사용하지 않는다. default로 java.net.HttpURLConnection(SimpleClientHttpRequestFactory)을 사용한다. SimpleClientHttpRequestFactory의 Connection 방식은 매번 새로운 Connection을 만들어서 통신한다. 이 방식은 계속해서 통신을 해야하는 서비스 구조에서는 효율적이지 못하다.

그래서 Connection Pool을 적용하기 위해서 HttpComponentsClientHttpRequestFactory를 SimpleClientHttpRequestFactory대신 사용한다.

@Configuration
public class RestConfig {
    @Bean
    Public RestTemplate restTemplate() {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setConnection(5000); // 5초
        factory.setReadTimeout(5000); // 5초
        
        HttpClient httpClient = HttpClientBuilder.create()
                    .setMaxConnTotal(100) // maxConnTotal: 연결을 유지할 최대 숫자
                    .setMaxConnPerRoute(5) // maxConnPerRoute: 특정 경로당 최대 숫자
                    .setConnectionTimeToLive(5, TimeUnit.SECONDS) // keep - alive
                    .build();
        factory.setHttpClient(httpClient);

        return new RestTemplate(factory);
    }
}

이렇게 하면 Connection을 유지하면서 빠른 통신이 가능해진다.

 

재시도(Retry) 설정하기

API 통신은 네트워크 등과 같은 이슈로 간헐적으로 실패할 수 도 있다. 한번 호출하여 실패했을 때 바로 실패 응답을 전달하는 것보다 일정 횟수만큼 요청을 재시도하는 것이 좋다.

Spring은 retry를 편리하게 구현하도록 spirng-retry 프로젝트에 RetryTemplate를 만들어두었는데, RestTemplate의 인터셉터에 적용하면 손쉽게 재시도 로직을 구현할 수 있다.

 

RetryTemplate를 적용하기 위해서 spring-retry 프로젝트의 의존성을 추가해주어야 한다.

implementation 'org.springframework.retry:spring-retry:1.2.5.RELEASE'
@Configuration
public class RestConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(5))
                .additionalInterceptors(clientHttpRequestInterceptor())
                .build();
    }
    
    public ClientHttpRequestInterceptor clientHttpRequestInterceptor() {
        return (request, body, execution) -> {
            RetryTemplate retryTemplate = new RetryTemplate();
            retryTemplate.setRetryPolicy(new SimpleRetryPolicy(3));
            try {
                return retryTemplate.execute(context -> execution.execute(request, body));
            } catch (Throwable throwable) {
                throw new RuntimeException(throwable);
            }
        };
    }
}

 

요청/응답 로깅(Logging) 설정하기

운영환경에서는 요청과 응답이 정상적으로 처리되었는지 확인이 필요하다. 이번에도 RestTemplate에 인터셉터를 적용해 요청과 응답 내용을 로그로 남겨주자.

@Configuration
public class RestConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(5))
                .additionalInterceptors(clientHttpRequestInterceptor(), new LoggingInterceptor())
                .requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
                .build();
    }
    
    public ClientHttpRequestInterceptor clientHttpRequestInterceptor() {
        return (request, body, execution) -> {
            RetryTemplate retryTemplate = new RetryTemplate();
            retryTemplate.setRetryPolicy(new SimpleRetryPolicy(3));
            try {
                return retryTemplate.execute(context -> execution.execute(request, body));
            } catch (Throwable throwable) {
                throw new RuntimeException(throwable);
            }
        };
    }
    
    @Slf4j
    static class LoggingInterceptor implements ClientHttpRequestInterceptor {

        @Override
        public ClientHttpResponse intercept(HttpRequest req, byte[] body, ClientHttpRequestExecution ex) throws IOException {
            final String sessionNumber = makeSessionNumber();
            printRequest(sessionNumber, req, body);
            ClientHttpResponse response = ex.execute(req, body);
            printResponse(sessionNumber, response);
            return response;
        }

        private String makeSessionNumber() {
            return Integer.toString((int) (Math.random() * 1000000));
        }

        private void printRequest(final String sessionNumber, final HttpRequest req, final byte[] body) {
            log.info("[{}] URI: {}, Method: {}, Headers:{}, Body:{} ",
                    sessionNumber, req.getURI(), req.getMethod(), req.getHeaders(), new String(body, StandardCharsets.UTF_8));
        }

        private void printResponse(final String sessionNumber, final ClientHttpResponse res) throws IOException {
            String body = new BufferedReader(new InputStreamReader(res.getBody(), StandardCharsets.UTF_8)).lines()
                    .collect(Collectors.joining("\n"));

            log.info("[{}] Status: {}, Headers:{}, Body:{} ",
                    sessionNumber, res.getStatusCode(), res.getHeaders(), body);
        }
    }
}

여기서 주목할 점은 RestTemplate 빌더에서 BufferingClientHttpRequestFactory가 사용된다는 것이다. 위와 같이 구현된 인터셉터는 로깅을 위해 응답 Stream(Inputstream)을 먼저 읽은다. 이후에 애플리케이션에서도 응답 값을 위해 다시 스트림을 읽는데, 이미 Stream의 데이터들이 컨슘된 상태라 데이터가 없어 에러가 발생한다. 이러한 문제를 방지하기 위해 추가해주는 것이 바로 BufferingClientHttpRequestFactory인데, BufferingClientHttpRequestFactory를 추가하면 스트림의 내용을 메모리에 버퍼링해둠으로써 여러 번 읽을 수 있다. 그래서 인터셉터에서는 로그를 남기고, 애플리케이션에서는 응답 결과를 얻을 수 있는 것이다.

위와 같이 설정된 RestTemplate을 실행해보면 다음과 같이 로그가 남는 것을 볼 수 있다. 

RestConfig$LoggingInterceptor : [123456] URI: localhost:8091/name, Method: GET, Headers:[Accept:"application/json, application/*+json", Content-Length:"0"], Body: 
RestConfig$LoggingInterceptor : [123456] Status: 200 OK, Headers:[Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Tue, 07 Jun 2022 14:25:00 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"], Body:{"name":"nero"}

남기는 로그 메세지나 재시도 횟수 등은 각자의 상황에 맞게 변경이 필요할 수 있다. 그 외에도 Gzip 비활성화나 Dns 캐시가를 shuffle을 사용하도록 변경하는 등의 작업을 하면 성능이 좋아진다. 이 외에도 써킷브레이커 적용 등 고도화할 부분이 남아있지만 이에 대해서는 나중에 다시 알아보도록 하자.

 

RestTemplate 보다는 OpenFeign

RestTemplate는 직접 API 호출 코드를 작성해야하므로 번거롭다. Netflix에서 시작된 OpenFeign이라는 도구를 사용하면 Spring Data JPA처럼 인터페이스와 어노테이션 기반으로 외부 API를 호출할 수 있다.

@FeignClient(name = "ExchangeRateOpenFeign", url = "${exchange.current.api.url}")
public interface ExchangeRateOpenFeign {

    @GetMapping
    ExchangeRateResponse call(@RequestHeader Spring apiKey,
                              @RequestParam Currency source,
                              @RequestParam Currency currencies);
}

 

NOTE: As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.
RestTemplate는 Spring 5.0 이상에서 deprecated되어 WebClient를 권장하고 있다.

 

 

 출처 

https://renuevo.github.io/spring/resttemplate-thread-safe/

 

[Spring] RestTemplate는 Thread Safe할까?

RestTemplate은 Spring에서 지원하는 http를 유용하게 쓸 수 있는 템플릿입니다 결론부터 말씀 드리자면 RestTemplate은 Thread Safe하게 설계 되어 만들어 졌습니다 Spring3 : RestTemplate 하지만 프로젝트에서 한

renuevo.github.io

https://mangkyu.tistory.com/256

 

[Spring] RestTemplate 타임아웃(Timeout), 재시도(Retry), 로깅(Logging) 등 설정하기

Spring 프레임워크에서는 외부 API와 통신하기 위한 RestTemplate을 구현해두었습니다. 이번에는 기본적으로 주어지는 RestTemplate에 부가적인 설정을 더해 고도화해보도록 하겠습니다. 1. RestTemplate 타

mangkyu.tistory.com

https://nesoy.github.io/articles/2020-05/RestTemplate

 

RestTemplate에 대해

RestTemplate Spring에서 제공하는 Rest Client 현재는 Deprecated되어 WebClient로 가이드를 하고 있다. NOTE: As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, co

nesoy.github.io

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형
보통 스프링에서 빈 주입을 할 때, 다음과 같이 사용을 한다.
@Component
@RequiredArgsConstructor
public class MadExample {
    private final HelloService helloService;
    ... 생략 ...
}
lombok의 @RequiredArgsConstructor 을 사용하여 초기화 되지 않은 final 필드나 @NonNull이 붙은 필드에 대해 생성자를 생성해 주어, 주로 의존성 주입(Dependency Injection) 편의성을 위해 사용한다.
어떠한 빈(Bean)에 생성자가 오직 하나만 있고, 생성자의 파라미터 타입이 빈으로 등록가능한 존재라면, 이 빈은 @Autowired 어노테이션 없이도 의존성 주입이 가능하다.
 
회사 동료는 필드 주입으로 되어있던 코드들을 생성자 주입으로 변경하는 중이었다. (생성자 주입을 권장하는 이유) 빈에 @RequiredArgsConstructor 나 @AllArgsConstructor를 사용했지만 빈 생성 오류가 난다는 연락을 받았다.
 

Lombok 

1) 프로젝트에 lombok 라이브러리가 설치되어 있는지 확인
2) IDE(IntelliJ)에 lombok 플러그인이 추가되어있는지 확인
  • Settings > Plugins > Lombok Plugin 설치

  • Settings > Build, Execution, Deployment > Compiler > Annotation Processors Enable annotation processing 활성화

스프링 버전

스프링 버전과 빈 주입의 상관관계가 있을 것 같아서 찾아보았다. 아래처럼 Spring 4.3부터 생성자가 오직 하나만 있고, 생성자의 파라미터 타입이 빈으로 등록가능한 존재라면, 이 빈은 @Autowired 어노테이션을 생략할 수 있다고 한다.
 
Starting with Spring 4.3, if a class, which is configured as a Spring bean, has only one constructor, the @Autowired annotation can be omitted and Spring will use that constructor and inject all necessary dependencies.
As of Spring Framework 4.3, an @Autowired annotation on such a constructor is no longer necessary if the target bean defines only one constructor to begin with. However, if several constructors are available and there is no primary/default constructor, at least one of the constructors must be annotated with @Autowired in order to instruct the container which one to use. See the discussion on constructor resolution for details.
 
해당 프로젝트의 스프링 버전은 4.1.2였고, @Autowired 생략이 불가능해 생성자 주입을 할 때 @Autowired를 명시적으로 작성해줘야 한다.

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

의존성을 주입하는 방법

스프링 프레임워크에서 사용하는 DI(Dependency Injection) 방법은 3가지다.
 

1. 필드 주입 (Field Injection)

필드에 @Autowired 어노테이션을 붙여주면 자동으로 의존성이 주입된다.
@Component
public class MadExample {

    @Autowired
    private HelloService helloService;
}

2. 수정자 주입 (Setter Injection)

꼭 setter 메서드일 필요는 없다. 하지만 일관성과 명확한 코드를 만들기 위해서 정확한 이름을 사용하는 것을 추천한다.
@Component
public class MadExample {

    private HelloService helloService;
    
    @Autowired
    public void setHelloService(HelloService helloService) {
        this.helloService = helloService;
    }
}

3. 생성자 주입 (Constructor Injection)

스프링 프레임워크 4.3부터 의존성 주입으로부터 클래스를 완벽하게 분리할 수 있다. 단일 생성자인 경우에는 @Autowired 어노테이션을 붙이지 않아도 되지만 생성자가 2개 이상인 경우에는 생성자에 어노테이션을 붙여주어야 한다.
@Component
public class MadExample {

    private final HelloService helloService;
    
    // 단일 생성자인 경우는 추가적인 어노테이션이 필요없다.
    public MadExample(HelloService helloService) {
        this.helloService = helloService;
    }
}
필드 주입을 사용하면 다음과 같은 문구를 볼 수 있다.
Field injection is not recommended

Inspection info: Spring Team recommends: "Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies".
핵심은 생성자 주입방법을 사용하라는 것이다.
다른 주입방법보다 생성자 주입을 권장하는 이유는 무엇일까?
 

왜 생성자 주입을 권장할까?

필드 주입이나 수정자 주입과 다르게 생성자 주입이 주는 장점에 대해 알아보자.
 

1. 순환 참조를 방지할 수 있다.

개발을 하다보면 여러 컴포넌트 간에 의존성이 생긴다. 그 중에서 A가 B를 참조하고, B가 다시 A를 참조하는 순환참조도 발생할 수 있는데, 아래 코드를 통해 어떤 경우인지 보자.
우선 두 개의 서비스 레이어 컴포넌트를 정의한다. 그리고 서로 참조하게 한다. 조금 더 극단적인 상황을 만들기 위해 순환 참조하는 구조에 더불어 서로의 메서드를 순환 호출하도록 한다. 빈이 생성된 이후에 비즈니스 로직으로 인하여 서로의 메서드를 순환 참조하는 형태이다. 실제로는 이러한 형태가 되어서는 안되며, 직접적으로 서로를 계속해서 호출하는 코드는 더더욱 안된다.
@Service
public class MadPlayService {
    
    @Autowired
    private MadLifeService madLifeService;
    
    public void sayMadPlay() {
        madLifeService.sayMadLife();
    }
}

@Service
public class MadLifeService {
    
    @Autowired
    private MadPlayService madPlayService;
    
    public void sayMadLife() {
        madPlayService.sayMadPlay();
    }
}

@SpringBootApplication
public class DemoApplication implements CommandLineRunner {

    @Autowired
    private MadLifeService madLifeService;
    @Autowired
    private MadPlayService madPlayService;
    
    @Override
    public void run(String... args) {
        madPlayService.sayMadPlay();
        madLifeService.sayMadLife();
    }
    
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
아무런 오류없이 정상적으로 구동되었다. 물론 run메서드의 내용이 수행되면서 다음과 같은 오류로 종료되었다.
java.lang.StackOverflowError: null
at com.example.demo.GreetService.sayGreet(GreetService.java:12) ~[classes/:na]
at com.example.demo.HelloService.sayHello(HelloService.java:12) ~[classes/:na]
at com.example.demo.GreetService.sayGreet(GreetService.java:12) ~[classes/:na]
at com.example.demo.HelloService.sayHello(HelloService.java:12) ~[classes/:na]
at com.example.demo.GreetService.sayGreet(GreetService.java:12) ~[classes/:na]
문제는 어플리케이션이 아무런 오류나 경고 없이 구동된다는 것이다. 메서드 실행 시점에 문제를 발견할 수 있다.
그렇다면 생성자 주입을 사용한 경우는 어떻게 될까?
@Service
public class MadPlayService {
    
    private final MadLifeService madLifeService;
    
    public MadPlayService(MadLifeService madLifeService) {
        this.madLifeService = madLifeService;
    }
    
    public void sayMadPlay() {
        madLifeService.sayMadLife();
    }
}

@Service
public class MadLifeService {
    
    private final MadPlayService madPlayService;
    
    public MadLifeService(MadPlayService madPlayService) {
        this.madPlayService = madPlayService;
    }
    
    public void sayMadLife() {
        madPlayService.sayMadPlay();
    }
}

BeanCurrentlyInCreationException이 발생하며 어플리케이션이 구동조차 되지않는다. 따라서 발생하는 오류를 사전에 알 수 있다.

Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
|  madLifeService defined in file [~~~/MadLifeService.class]
↑                    ↓
|  madPlayService defined in file [~~~/MadPlayService.class]
└─────┘
실행 결과에 차이가 발생하는 이유는 생성자 주입은 필드 주입이나 수정자 주입과는 빈 주입 순서가 다르기 때문이다.
수정자 주입과 필드 주입은 주입을 받으려는 빈의 생성자를 호출하여 빈을 찾거나 빈 팩터리에 등록한다. 그 후에 생성자 인자에 사용하는 빈을 찾거나 만든다. 그 이후에 주입하려는 빈 객체의 수정자를 호출하여 주입한다. 즉, 빈을 먼저 생성한 후에 주입을 한다.
생성자 주입은 생성자로 객체를 생성하는 시점에 필요한 빈을 주입한다. 먼저 생성자의 인자에 사용되는 빈을 찾거나 빈 팩터리에 만든다. 그 후에 찾은 인자 빈으로 주입하려는 빈의 생성자를 호출한다. 즉, 빈을 먼저 생성하지 않는다.
그렇기 때문에 순환 참조는 생성자 주입에서만 문제가 된다. 객체 생성 지점에 빈을 주입하기 때문에 서로 참조하는 객체가 생성되지 않은 상태에서 빈을 참조하기 때문에 오류가 발생한다.

2. 불변성 (Immutability)

필드 주입과 수정자 주입은 해당 필드를 final로 선언할 수 없다. 따라서 초기화 후에 빈 객체가 변경될 수 있지만, 생성자 주입의 경우는 다르다. 필드를 final로 선언할 수 있다. 

3. NullPointer Exception 방지

스프링에서는 강제되는 의존성의 경우는 생성자 주입을 사용하고 선택적인 경우에는 수정자 주입 형태를 사용하는 것을 권장하고 있다. 이 맥락에서 불변 객체나 null이 아님을 보장할 때는 반드시 생성자 주입을 사용해야 한다. 
@Service
public class MadPlayService {
    
    @Autowired
    private MadPlayRepository madPlayRepository;
    
    public void someMethod() {
        // final이 아니기 때문에 값을 변경할 수 있다.
        madPlayRepository = null;
        // NullPointerException이 발생한다.
        madPlayRepository.call();
    }
}

생성자 주입을 사용한다면 이와 같은 상황을 컴파일 시점에 방지할 수 있다.

4. 단일 책임 원칙 관점 (SRP)

한 개의 컴포넌트가 수많은 의존성을 가질 수 있다. 필드 주입을 사용하면 제한이 없다. 의존하는 객체가 많다는 것은 하나의 클래스가 많은 책임을 가진다는 의미이다.
생성자 주입을 사용하게 되는 경우 생성자의 인자가 많아짐에 따라 복잡한 코드가 됨을 쉽게 알 수 있고, 리팩토링해 역할을 분리하여 코드의 품질을 높이는 활동의 필요성을 더 쉽게 알 수 있다. (물론 lombok의 @RequiredArgsConstuctor을 쓰면 다음과 같은 장점은 사라진다.)
@Component
public class MadComponent {

    @Autowired
    private FirstComponent firstComponent;
    @Autowired
    private SecondComponent secondComponent;
    @Autowired
    private NumberComponent numberComponent;
    @Autowired
    private SomeComponent someComponent;
    @Autowired
    private StrangeComponent strangeComponent;
    
    ...생략...
}

5. DI 컨테이너와 결합도가 낮아 테스트하기 좋다.

생성자 주입을 사용하게 되면 테스트 코드를 조금 더 편리하게 작성할 수 있다. DI의 핵심은 관리되는 클래스가 DI 컨테이너에 의존성이 없어야 한다는 것이다. 즉, 독립적으로 인스턴스화가 가능한 POJO(Plain Old Java Object)여야 한다는 것이다. DI 컨테이너를 사용하지 않고서도 단위 테스트에서 인스턴스화할 수 있어야 한다.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SpringInjectionApplication.class)
class ConstructorInjectionTest {
    @Autowired
    private Water water;

    @Test
    @DisplayName("생성자 주입 테스트 코드")
    void constructor_injection() {
        assertThat(water).isNotNull();
        assertThat(water.getTeaBag()).isNotNull();
        assertThat(water.getTeaBag()).isInstanceOf(Chamomile.class);
    }
}

위와 같이 테스트도 가능하지만, 아래와 같이 스프링 컨테이너의 도움 없이 테스트 가능하다.

class ConstructorInjectionTest {
    @Test
    @DisplayName("생성자 주입 사용 시, 스프링 컨테이너에 의존하지 않고 테스트할 수 있다.")
    void constructor_injection_without_dependence() {
        Water water = new Water(new GreenTea());
        assertThat(water).isNotNull();
        assertThat(water.getTeaBag()).isNotNull();
        assertThat(water.getTeaBag()).isInstanceOf(GreenTea.class);
    }
}

 

 

 출처 

https://madplay.github.io/post/why-constructor-injection-is-better-than-field-injection

 

생성자 주입을 @Autowired를 사용하는 필드 주입보다 권장하는 하는 이유

@Autowired를 사용하는 의존성 주입보다 생성자 주입(Constructor Injection)을 더 권장하는 이유는 무엇일까?

madplay.github.io

https://velog.io/@dahye4321/Constructor-%EC%A3%BC%EC%9E%85%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

1. ResponseEntity

- Spring 프레임워크에는 HTTP Request 혹은 Response을 나타내기 위해 HttpEntity 클래스가 존재한다.

- HttpEntity 클래스는 HttpHeader, HttpBody가 있다.

- HttpEntity를 상속하여 추가적으로 HttpStatus 속성이 있는 클래스가 RequestEntity, ResponseEntity 클래스가 있다.

 

[사용법]

1) static method를 사용하여 ResponseEntity 반환

@GetMapping("/v1")
public ResponseEntity<String> getV1() {
    return ResponseEntity.ok("hello world!");
}
// response

HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 12
Data: Wed, 29 Mar 2023 16:17:22 GMT
Keep-Alive: timeout=60
Connection: keep-alive

hello world!

- ResponseEntity에는 자주 쓰이는 HttpStatus로 쉽게 인스턴스를 생성할 수 있는 static 메서드를 제공한다.

- OK(200), CREATED(201), NO_CONTENT(204), NOT_FOUND(404) 등

 

2) 생성자를 이용한 ResponseEntity 반환

@GetMapping("/v2")
public ResponseEntity<String> getV2() {
    return new ResponseEntity("hello world!", HttpStatus.OK);
}
// response

HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 12
Data: Wed, 29 Mar 2023 16:17:22 GMT
Keep-Alive: timeout=60
Connection: keep-alive

hello world!

- ResponseEntity는 여러 생성자를 지원하여 HttpBody, HttpHeader, HttpStatus 케이스를 유연하게 오버로딩 되어있다.

 

3) HttpHeader 추가하기

@GetMapping("/v3")
public ResponseEntity<String> getV3() {
    MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
    headers.add("test", "header-value");
    
    return new ResponseEntity("hello world!", headers, HttpStatus.OK);
}
// response

HTTP/1.1 200
test: header-value
Content-Type: text/plain;charset=UTF-8
Content-Length: 12
Data: Wed, 29 Mar 2023 16:17:22 GMT
Keep-Alive: timeout=60
Connection: keep-alive

hello world!

- MultiValueMap 타입을 가지는 Header를 정의할 수 있다.

 

2. @ResponseStatus

- HttpStatus를 다른 방식으로 표현한 어노테이션이다.

- 컨트롤러단에서 반환하는 Body를 감싸 @ResponseStatus에 정의된 HttpStatus를 추가할 수 있지만 주로 예외처리에서 쓰인다.

 

[사용법]

1) Controller 에서 사용

@GetMapping("/v4")
@ResponseStatus(value = HttpStatus.OK)
public String getV4() {
    return "hello world!";
}
// response

HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 12
Data: Wed, 29 Mar 2023 16:17:22 GMT
Keep-Alive: timeout=60
Connection: keep-alive

hello world!

- ModelAndView로 반환하는 @Controller가 아닌 텍스트 Body를 반환하는 @RestController 기준으로 작성되었다.

- @ResponseStatus에 정의된 HttpStatus로 반환된다.

- ResponseEntity 클래스처럼 유연하지 않다. 커스텀 header 정의도 불가능하며 반환값에 추가적인 작업도 불가능하다.

- default 값은 HttpStatus.INTENAL_SERVER_ERROR(500) 이다.

 

2) Exception 으로 활용

@ReponseStatus(value = HttpStatus.NOT_FOUND)
@Slf4j
public class MyResponseCustomException extends RuntimeException {
    
    public MyResponseCustomException(String message) {
        super(message);
        log.error(message);
    }
}

- 기본적인 Spring Web MVC는 로직상 Exception이 발생하면 HttpStatus.INTERNAL_SERVER_ERROR를 반환한다.

- Exception에 @ResponseStatus를 정의하면 해당 HttpStatus로 반환할 수 있다.

 

3) @ControllerAdvice로 활용

@RestControllerAdvice
public class MyResponseGlobalExceptionHandler {

    @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
    @ExceptionHandler(MyResponseCustomException.class)
    public String myResponseGlobalExceptionHandler(MyResponseCustomException ex) {
        return ex.getMessage();
    }
}

- @ControllerAdvice 혹은 @ExceptionHandler에서도 활용할 수 있다.

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

환경: Spring JPA

문제: PK가 같은 엔티티 2개가 각각 준영속, 영속상태일 때 비교가 되지 않음

해결: equals, hashCode 메서드를 재정의

Object 클래스의 equals 특성

1. reflexive (반사성)
x.equals(x)는 항상 참이어야 한다.

2. symmetric (대칭성)
x.equals(y)가 참이라면 y.equals(x) 역시 참이어야 한다.

3. transitive (추이성)
x.equals(y)가 참이고 y.equals(z)가 참일 때, x.equals(z) 역시 참이어야 한다.

4. consistent (일관성)
x.equals(y)가 참일 때 equals 메서드에 사용된 값이 변하지 않는 이상 몇 번을 호출해도 같은 결과가 나와야 한다.

5. x가 null이 아닐 때 x.equals(null)은 항상 거짓이어야 한다.

 

JPA (Hibernate)의 Entity 특징

@Entity가 붙은 클래스의 instance A와 B가 있다고 하자.

이 둘의 instance는 동일한 데이터베이스, 테이블의 같은 row를 instance로 만들었다. 즉, 같은 데이터를 기반하여 만들어진 instance이다.

이 경우 A == B 란 코드에서 true를 반환한다.

이 동작이 가능한 이유는 Persistence Context (영속성 컨텍스트)의 기능 때문이다.

 

재정의해야 하는가?

다음과 같은 엔티티가 있다고 하자.

@Entity
public class Item {
    @Id @GeneratedValue(strategy = GeneratedType.IDENTITY)
    private Long id;

    private String name;
    private Long price;
    private Long stockQuantity;

    ...
}

이 객체는 별다른 equals, hashCode 메서드를 재정의하지 않았다.

다음과 같은 테스트 코드를 작성하여 두 Item을 비교해 보았다.

@Test
void equalsTest() {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    
    Item item1 = Item.builder()
                    .namer("Cake")
                    .price(6000L)
                    .stockQuantity(10L).build();
    
    entityManager.persist(item1);
    transaction.commit();
    entityManager.clear();
    
    Item item2 = entityManager.find(Item.class, item1.getId()); // PK로 찾는다.
    assertEquals(item1, item2); // org.opentest4j.AssertionFailedError 발생!!!
}

결과는 위와 같이 실패한다.

 

일반적으로 한 엔티티 매니저의 영속성 컨텍스트에서 1차 캐시를 이용해 같은 ID의 엔티티를 항상 같은 객체로 가지고 올 수 있다. 하지만 위처럼 1차 캐시를 초기화한 후, 다시 데이터베이스에서 동일한 엔티티를 읽어오는 경우 초기화 전에 얻었던 item1과 item2 객체가 서로 다르다.

 

이는 위에서 언급한 equals 메서드의 consistent 원칙을 위반하게 된다. 엔티티는 그 본질이 자바 객체라기보단 데이터베이스 테이블의 레코드에 가깝기 때문에 이 Item 엔티티 객체의 필드(id, name, price, stockQuantity)가 동일하다면 같은 레코드, 즉 객체라고 판단해야 하는 것이다. 이 경우 Object의 equals 메서드로는 해결할 수 없기 때문에 equals 메서드 그리고 관례에 따라 hashCode 메서드를 재정의해야 한다.

 

어떻게 재정의해야 하는가?

1. 기본키로 구현하기

2. PK를 제외하고 구현하기

3. 비즈니스 키를 사용하여 구현하기

 

1. 기본키로 구현하기

모든 데이터베이스 레코드, 즉 엔티티는 각자 고유한 기본키를 가진다. 이는 데이터베이스에 의해 유일성이 보장되기 때문에 equals 메서드를 작성할 수 있다.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
    Item item = (Item) o;
    return id != null && Objects.equals(id, item.id);
}

@Override
public int hashCode() {
    int result = id != null ? id.intValue() : 0;
    return result;
}

이 구현은 다음과 같은 특징이 있다.

1. id == null 인 경우 (비영속) 아예 동일하지 않다고 명시한다.

즉, repository.save()를 호출하지 않은 instance는 동등성을 아예 사용할 수 없다.

2. 객체가 영속화되기 전까지 Hibernate는 PK를 할당하지 않는다.

준영속 상태의 두 엔티티를 비교하게 되면, 준영속 상태이기 때문에 id 값이 모두 null이다. 두 엔티티가 다른 객체더라도 id 값이 null로 같아 같은 객체로 인식한다.

 

2. PK를 제외하고 구현하기

엔티티 클래스의 모든 필드에 Objects.equals 메서드를 적용하여 비교하는 방식으로 구현할 수 있다.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
    if (!(o instanceof User)) return false;

    Item item = (Item)o;
    return  Objects.equals(name, item.name) &&
            Objects.equals(price, item.price) &&
            Objects.equals(stockQuantity, item.stockQuantity);
}

@Override
public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + (price != null ? price.hashCode() : 0);
    result = 31 * result + (stockQuantity != null ? stockQuantity.hashCode() : 0);
    return result;
}

이 구현에서는 다음과 같은 문제가 발생할 수 있다.

1. 임의 영속성 컨텍스트의 instance를 수정한다면 다른 영속성 컨텍스트의 instance와 동일하지 않다. (변경되는 값이므로)

2. 속성의 조합이 유일성(unique)을 보장하지 않는다면, 전혀다른 row와 동일한 instance로 판단될 수 있다.

 

2. 비즈니스 키를 사용하여 구현하기

비즈니스 키는 다음과 같은 특성을 갖는다.

1. 변경할 수 없는 것은 아니지만, 변경할 일이 거의 없다.

2. Entity 클래스에는 반드시 비즈니스 키가 있어야 한다.

3. application에서 특정 record를 유일하게 식별하는데 사용한다.

 

비즈니스 키를 식별하는 기준

변경이 불가능한 필드는 아니지만, 변경의 횟수가 다소 적고, DB의 제약조건을 통해 유일성(UNIQUE)을 보장한다.

 

결론

1. 정의한 Entity를 Set과 같은 collection에 담아 관리할 가능성이 있는가? 없다면 application이 확정되는 과정에서도 계속해서 없음을 보장할 수 있는가?

=> PK를 사용하여 구현한다.

2. 영속, 비영속 상태의 entity를 비교할 가능성이 있는가? 있다면 각 속성의 조합으로 식별성을 갖출 수 있는가?

=> PK를 제외한 속성들로 구현한다.

3. 비즈니스 키를 사용하기 적합한 상황인가?

- 예를 들어 개인 식별 정보를 포함한다. 이름 + 핸드폰 / 이름 + 이메일

- 적은 개수의 속성으로도 식별성을 어느정도 보장할 수 있다.

 

 

 참고 

https://blog.yevgnenll.me/posts/jpa-entity-eqauls-and-hashcode-equality

 

Jpa Entity 의 Equals, 객체 동일성과 동등성, Lombok 을 써도 될까?

이전 면접에서 JPA entity 의 equals 와 hashCode 를 어떻게 구현했는지 묻는 질문이 나왔었다. 당시에는 Lombok 을 사용하고 있었고 자연스럽게 @EqualsAndHashCode 를 사용했기 때문에 이 부분에 대해 고민해

blog.yevgnenll.me

https://velog.io/@park2348190/JPA-Entity%EC%9D%98-equals%EC%99%80-hashCode

 

JPA Entity의 equals와 hashCode

스프링 프로젝트에서 ORM 기술로 JPA를 활용하던 도중 equals, hashCode 메서드를 재정의할 경우가 있었는데 이에 대한 고민 과정을 적어보고자 한다.일단 왜 재정의해야 하냐라는 의문이 들 수 있겠

velog.io

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !