네로개발일기

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

'전체 글'에 해당되는 글 194건


반응형

 이전 글 

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

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

반응형

클라이언트와 WAS 사이에 리버스 프록시 서버를 둔다. 클라이언트는 리버스 프록시 서버에 요청하고, WAS는 리버스 프록시로부터 사용자의 요청을 대신 받는다. 클라이언트는 리버스 프록시 서버 뒷단의 WAS의 존재를 알지 못한다. 이로 인해 보안이 한층 강화되었다.

 

리버스 프록시 서버에 SSL 인증서를 발급하여 HTTPS를 적용한다. WAS 서버가 여러대로 늘어나도 SSL 인증서 발급은 추가로 하지 않아도 되니 확장성이 좋다. 또한 WAS 서버가 SSL 요청을 처리하는데 비용도 들지 않는다.

 

리버스 프록시 서버는 Nginx를 사용한다. CA로 무료 SSL 인증서 발급기관인 Let's Encrypt를 사용한다. 또한, 간단한 SSL 인증서 발급 및 Nginx 환경설정을 위해 Certbot을 사용한다.

리버스 프록시란? 클라이언트 요청을 대신 받아 내부 서버로 전달해주는 것
- 서버의 응답을 캐시에 저장하여 서버에 요청하지 않고, 응답해서 리소스를 절약한다.
- 내부의 WAS를 보호한다.
- 많은 요청을 처리하기 위해 여러 대의 서버에 부하를 분산시킬 수 있다.

리버스 프록시인 Nginx를 사용하면 클라이언트 요청을 프록시 서버에 분산하기 위해 로드밸런싱으로 부하가 줄여줄 수 있고, 분산처리 또한 가능하며, 웹 서버의 SSL 인증도 적용할 수 있다.

 

WAS 서버 세팅

준비된 서버에 WAS를 띄우자. 이 포스팅에선 8080포트로 어플리케이션 서버를 실행한다.

 

리버스 프록시 서버 세팅

Nginx 설치

nginx 패키지를 설치한다.

$ sudo apt update

# Nginx 설치
$ sudo apt install nginx

# Nginx 실행
$ sudo service nginx start

Nginx 리버스 프록시 설정

리버스 프록시를 위한 Nginx 설정을 해줄 것이다. /etc/nginx/conf.d 디렉토리로 이동해서 Nginx를 위한 설정파일을 생성하자.

sites-available, sites-enabled는 더이상 사용하지 않는 Nginx 설정방법이라고 한다. conf.d 디렉터리에 Nginx 설정 파일을 만들고 관리한다.
만약 sites-available과 sites-enabled에 기본 설정 파일이 있다면 제거하자. 또한 conf.d에 기본으로 default.conf 파일이 존재하면, 그 파일을 수정해서 설정하자.
$ cd /etc/nginx/conf.d
$ vi default.conf

위 명령을 실행해서 default.conf 파일을 생성하자.

server {
    listen 80;
    server_name your.domain.com;

    location / {
        proxy_pass http://192.168.XXX.XXX;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
    }
}

server_name 은 SSL을 적용할 도메인을 입력한다. 후술할 Certbot은 이 server_name을 기준으로 Nginx 설정파일을 찾고 여기에 HTTPS에 대한 설정을 자동으로 추가해준다.

proxy_pass 에는 프록시 서버가 클라이언트 요청을 전달할 리얼 서버의 주소를 적는다. 리버스 프록시의 가장 큰 목적 중 하나는 실제 서버의 IP 주소를 클라이언트에게 노출하지 않기 위함이므로 여기서는 프라이빗 IP를 입력한다. (퍼블릭 IP를 입력해도 큰 차이는 없다.)

 

Certbot 설치 및 Let's Encrypt에서 SSL 인증서 발급

Certbot은 손쉽게 SSL 인증서를 자동 발급할 수 있도록 도와주는 도구이다. Certbot은 우분투의 snap라는 패키지 매니저를 사용하여 설피하는 것이 권장된다. (참고: Certbot 사이트 주소)

# snapd 설치
$ sudo yum install snapd
$ sudo snap install core
$ sudo snap refresh core
$ sudo snap install certbot --classic

아래 명령을 실행하여 SSL 인증서를 발급받는다.

$ sudo certbot --nginx

이메일을 입력하고, 이용약관에 동의 후 사용할 도메인을 입력한다. 이때, 적용할 도메인에 대한 A 레코드가 반드시 적용되어 있어야 한다.

이 과정을 거치면 Certbot은 Let's Encrypt를 통해 자동으로 SSL 인증서를 발급해온다.

위 명령을 수행 후, 아래처럼 뜬다면 잘 발급받은 것이다.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Your existing certificate has been successfully renewed, and the new certificate
has been installed.

The new certificate covers the following domains: https://your.domain.com # https가 설정된 도메인
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Subscribe to the EFF mailing list (email: jy.jeon@gmail.com).

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/your.domain.com/fullchain.pem # 공개키 경로이므로 기억해두자.
   Your key file has been saved at:
   /etc/letsencrypt/live/your.domain.com/privkey.pem # 비밀키 경로이므로 기억해두자.
   Your certificate will expire on 2021-08-15. To obtain a new or
   tweaked version of this certificate in the future, simply run
   certbot again with the "certonly" option. To non-interactively
   renew *all* of your certificates, run "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

출력된 문구를 보면 다음을 확인할 수 있다.

1. https가 설정된 도메인

https://your.domain.com

2. 공개키 경로

/etc/letsencrypt/live/your.domain.com/fullchain.pem

3. 비밀키 경로

/etc/letsencrypt/live/your.domain.com/privkey.pem

 

또한 우리가 작성한 Nginx의 default.conf 를 확인해보면 HTTPS를 위한 여러 설정이 자동으로 추가된 것을 볼 수 있다.

# 433 포트로 접근 시, ssl 적용 후 8080 포트로 요청을 전달
server {
    server_name your.domain.com;

    location / {
        proxy_pass http://192.168.XXX.XXX:8080;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header Host $http_host;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

# 80 포트로 접근 시, 433 포트로 리다이렉트
server {
    if ($host = your.domain.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    server_name your.domain.com;
    return 404; # managed by Certbot
}

기존에 작성한 리버스 프록시 관련 세팅을 유지한 채 80 포트 즉, HTTP로 들어온 요청중 host 헤더가 your.domain.com 이라면 443 즉, HTTPS로 301 Moved Permanently Status를 사용해서 리디렉션 해주는 것을 확인할 수 있다. host가 일치 하지 않으면 404 를 반환한다.

 

Crontab으로 SSL 인증서 자동 갱신 설정

Let's Encrypt에서 발급해주는 SSL 인증서는 90일짜리 단기 인증서이다. 90일마다 SSL 인증서를 수동으로 갱신해줘야한다.

# ssl 인증서 갱신 테스트
$ certbot renew --dry-run

# ssl 인증서 갱신
$ certbot renew

# ssl 인증서 만료일 확인
$ certbot certificates

 

리눅스 Crontab을 이용하여 ssl 인증서 갱신 자동화를 할 수 있다.

Crontab이란 리눅스에서 제공하는 기능으로 특정한 시간 혹은 특정한 주기로 명령을 수행하고 싶을 때 사용한다. 즉, Crontab은 스케줄링 도구이다. 아래 명령을 이용해서 cron job 하나를 생성하자.

$ crontab -e

vim 선택 후, 주석 가장 아래에 아래 내용을 추가하고 파일을 저장하자.

0 0 * * * certbot renew --post-hook "sudo service nginx reload"

매월, 매일 0시 0분에 certbot을 실행하여 SSL 인증서를 갱신하고, 갱신 이후 nginx의 설정파일을 reload 해주는 작업이다.

 

 참고 

https://frogand.tistory.com/109

 

[Nginx] Nginx 이해하기

문제 동시 접속자 폭발 네트워크 소켓을 통해 최대로 커버 가능한 동시 접속자의 작업수는 1만개였다. 이때 당시는 사용자 수 = 프로세스 개수(혹은 쓰레드 개수)로 서버 인프라가 설계되어 CPU,

frogand.tistory.com

 

 출처 

https://hudi.blog/https-with-nginx-and-lets-encrypt/

 

Nginx와 Let's Encrypt로 HTTPS 웹 서비스 배포하기 (feat. Certbot)

목표 우리의 목표 우리의 목표는 위 그림과 같다. 클라이언트와 WAS 사이에 리버스 프록시 서버를 둔다. 클라이언트는 웹서버처럼 리버스 프록시 서버에 요청하고, WAS는 리버스 프록시로부터 사

hudi.blog

https://gist.github.com/woorim960/dda0bc85599f61a025bb8ac471dfaf7a

 

Nginx를 이용하여 https 적용하는 법

Nginx를 이용하여 https 적용하는 법. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

https://devlog.jwgo.kr/2019/04/16/how-to-lets-encrypt-ssl-renew/

 

Let's Encrypt SSL 인증서 자동 갱신 설정 방법 · Tonic

사이트 운영에 도움을 주실 수 있습니다. 고맙습니다. --> Let's Encrypt SSL 인증서 자동 갱신 설정 방법 2019년 04월 16일 Let’s Encrypt에서 발급하는 인증서는 90일짜리 단기 인증서입니다. 3개월에 적어

devlog.jwgo.kr

 

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

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

반응형

 

비즈니스적으로 기본값이 있는 경우는 NOT NULL로 선언을 하지만 그런 경우가 아니라면 유연하게 대처하기 위해 Nullable로 선언을 한다.

테이블의 Boolean과 Number 타입의 컬럼을 Nullable로 선언하면 다양한 문제가 발생할 수 있어 추천하지 않는다.

 

1. 의미 혼란

컬럼을 Nullable로 설정할 경우, 기본값이 NULL 이 되므로, 의미가 혼란스러워 질 수 있다.

예를들어, boolean 컬럼의 값은 true, false, null 세 가지 상태가 될 수 있다. ('참', '거짓', '미확인')

일부 상황에서는 유용할 수 있지만, 대부분의 경우 논리적 복잡성이 추가되는 일이라 코드에서 이를 처리해야 할 경우 복잡성을 추가한다.

 

- boolean 타입의 컬럼은 null 값과 false가 어떤 의미 차이가 있는지

- Number 타입의 컬럼은 null 값과 0이 어떤 의미 차이가 있는지

 

이를 구분해서 사용해야 한다.

 

2. SQL의 복잡도

null을 가진 컬럼은 쿼리를 복잡하게 만든다.

2-1. Null 제외 후 계산

예를 들어, 다음과 같은 테이블의 row 데이터가 있다고 하자.

price - 1000
price - null
price - 0
price - 1000

이 상태의 평균값은 얼마일까?

4개 row의 총 합은 2,000이고 총 4개이니 500이 예상되지만

실제로 쿼리를 수행하면 666.6666...이 나온다.

SELECT AVG(price) FROM table;

PostgreSQL의 AVG() 함수는 평균을 계산할 때 Null 값은 자동으로 무시된다. (NULL 값은 계산에 포함하지 않는다.)

NULL값을 포함하여 계산을 할 경우, NULL 값을 특정 값으로 바꾸는 함수인 COALESCE()를 사용해야 한다.

SELECT AVG(COALESCE(price, 0)) FROM table;

 

물론, NULL 값을  제외하고 실제 채워진 값들에 한해서만 결과를 가져와야 하는 경우도 있다.

많은 집계함수에서 NULL 데이터의 포함/미포함에 대해 고민해야 하고, 그에 따른 추가적인 SQL 함수를 고려해야 한다.

 

매번 COALESCE를 통한 추가 SQL를 사용하거나, 잘못된 결과를 사용하거나 등의 위험을 항상 안고가야 할 정도로 NULL 값을 유지해야 할 필요가 있는지 고려해야 한다.

 

2-2. IS NULL

단순한 SQL 조회문을 만들 때도 이에 대한 고려가 항상 포함된다.

보통 false와 NULL 혹은 0과 NULL은 함께 조건에 사용될 때가 많다.

 

하지만, NULL 값을 조회하기 위해서 일반적인 비교 연산자 (=, <>, <, >, IN() 등)을 사용할 수 없으며 IS NULL 혹은 IS NOT NULL을 사용해야 한다.

그래서 false와 NULL을 함께 조회하려면 OR 연산자 쿼리를 작성해야만 한다.

SELECT * FROM users WHERE is_active IS FALSE OR is_active IS NULL;

하나의 상태값을 조회하기 위해서 쿼리가 복잡해질 수 있다.

 

3. 애플리케이션 코드 복잡성

컬럼에 NULL을 허용하면, 이 컬럼을 사용하는 애플리케이션 코드에서 항상 NULL 체크를 수행해야 한다.

이는 코드 복잡성을 증가시킨다.

결국 Nullable 컬럼의 데이터를 어플리케이션에서 조회하면 숫자 연산에 대해 0과 NULL 상태 모두 항상 조건을 걸거나 NULL -> 0 (또는 특정한 숫자값)으로 변경해야 한다.

val price = getPrice(); // nullable
val result = price ? price : 0;

 

결론

Number와 Boolean 타입에서 0과 NULL의 차이가 명확하거나 false와 NULL의 차이가 명확한 경우에만 Nullable로 선언하며 가능하면 항상 NOT NULL로 기본값을 보장하는 것이 좋다.

만약, null과 false, 0의 구분이 필요한 상황이면 그게 정말 null로 구분해야 하는 것인지 고민하고, 상태를 나타내는 Enum을 고려하는 것이 좋을 수도 있다.

 

예를들어, 합격 여부에 대한 항목이 필요하다고 하면

// AS-IS = boolean과 NULL을 함께 사용
pass.isPassed // 합격 여부
- null : 합격 발표 전
- false : 합격 발표 - 불합격
- true: 합격 발표 - 합격

// TO-BE = Enum을 사용
pass.status
- READY : 합격 발표 전
- FAIL : 합격 발표 - 불합격
- PASS : 합격 발표 - 합격

이에 대해서는 객체 생성단계에서 무조건 기본값을 할당하는 것이 좋다.

 

물론, NULL과 0의 차이가 명확하게 구분된 상황이라면 이에 대해 정확하게 주석을 남겨야 하며 이 주석의 범위는 테이블 컬럼 주석과 ORM 영역 모두에 해당한다.

 

 출처 

https://jojoldu.tistory.com/718 

 

Number와 boolean 은 최대한 Not Null로 선언하기

테이블 설계시 종종 받는 질문 중 하나가 Boolean과 Number 컬럼의 Not Null 유무이다. 비즈니스적으로 기본값이 있는 경우가 아니면 유연하게 하기 위해 nullable 로 선언하는 경우를 자주 본다. 테이블

jojoldu.tistory.com

 

728x90
반응형
blog image

Written by ner.o

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