네로개발일기

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

'programming language'에 해당되는 글 67건


반응형

클라이언트와 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

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

반응형

 

비즈니스적으로 기본값이 있는 경우는 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

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

반응형

메시지 큐란

메시지 큐(Message Queue)는 프로세스 또는 프로그램 간에 데이터를 교환할 때 사용하는 통신 방법 중 하나로, 메시지 지향 미들웨어(Message Oriented Middleware:MOM)를 구현한 시스템을 의미한다. 메시지 지향 미들웨어란 비동기 메시지를 사용하는 응용 프로그램들 사이에서 데이터를 송수신하는 것을 의미한다. 여기서 메시지란, 요청/응답/오류 메시지 혹은 단순한 정보 등의 작은 데이터가 될 수 있다.

 

메시지 큐는 메시지를 임시로 저장하는 간단한 버퍼라고 생각하면 된다. 메시지를 전송 및 수신하기 위해 중간에 메시지 큐를 둔다.

메시지 전송 시 생산자(Producer)로 취급되는 컴포넌트가 메시지를 메시지 큐에 추가한다. 해당 메시지는 소비자(Consumer)로 취급되는 또 다른 컴포넌트가 메시지를 검색하고 이를 사용해 어떤 작업을 수행할 때까지 메시지 큐에 저장된다. 각 메시지는 하나의 소비자에 의해 한 번만 처리될 수 있는데, 이러한 이유로 메시지 큐를 이용한 방식을 일대일통신이라고 부른다.

 

메시지 큐를 사용하는 경우는?

일반적인 클라이언트-서버 구조에서는 사용자가 요청을 하면 서버는 그에 대한 처리를 한 후 클라이언트에게 응답을 한다. 간단한 서버 구조에서는 굳이 메시지 큐를 사용할 필요가 없다. 사용자가 응답을 기다려야하는 경우, HTTP 요청을 바로 처리하지 않고 중간에 메시지 큐를 두는 경우는 바람직하지 않다. 또한, 메시지 큐를 적용하려면 다양한 메시지 큐 중에서 시스템의 목적에 맞는 것을 선정해야 한다. 이후 선정된 메시지 큐의 사용방법을 익혀 지원하는 다양한 옵션 중에 시스템이 추구하는 옵션을 설정해야 한다. 

왜 메시지 큐를 사용하며, 어떤 경우에 사용하는 걸까?

메시지 큐는 소비자(Consumer)가 실제로 메시지를 어느 시점에 가져가서 처리하는지는 보장하지 않는다. 언젠가 큐에 넣어둔 메시지가 소비되어 처리될 것이라고 믿는 것이다. 이러한 비동기적 특성때문에 메시지 큐는 실패하면 치명적인 핵심 작업보다는 어플리케이션의 부가적인 기능에 사용하는 것이 적합하다.

 

이메일 전송

어떤 웹 사이트의 비밀번호를 잊어버려서 임시 비밀번호를 받거나, 새로운 회원가입을 위한 인증 코드를 보내는 이메일 서비스는 어느정도의 응답 지연이 허용되며, 어플리케이션의 핵심 기능이 아닌 경우이므로 메시지 큐를 사용할 수 있다.

- 비밀번호 재설정을 위해 이메일을 발급하는 서비스, 회원가입을 위해 이메일을 발급하는 서비스 등

- 이메일 전송 전용 서비스는 이메일이 어느 서비스로부터 생산되었는지와 관계없이, 메시지 큐의 메시지를 하나씩 소비하고, 그저 이메일이 전송되어야 할 곳으로 이메일을 전송한다.

- 메시지 큐에 들어오는 메시지 수가 많아지는 경우 이메일 전송 전용 서비스 인스턴스를 더 둠으로써 확장할 수 있다. (확장성이 뛰어남)

 

블로그 포스팅

모든 블로그 사용자가 웹에 최적화되어 있거나, 용량이 작은 이미지만 업로드하는 것이 아니다. 블로그 사용자가 서비스의 응답 시간을 저해하지 않으면서 사용자들에게 유연성을 제공하는 방법으로, 사용자가 업로드한 모든 이미지를 게시하는 과정에서 즉각 처리가 아닌 사후 처리로 최적화한다. 

- 사용자가 고용량의 이미지가 포함된 블로그 포스팅을 한다.

- 이미지는 저장소에 전송된다.

- 업로드된 이미지에 대한 정보가 포함된 메시지를 이미지 최적화 서비스의 메시지 큐에 담는다.

- 이미지 최적화 서비스는 저장소에서 이미지를 가져와 최적화 후 이미지를 대체한다.

 

메시지 큐의 이점

1. 비동기(Asynchronous)

메시지 큐는 생산된 메시지의 저장, 전송에 대해 동기화 처리를 진행하지 않고, 큐에 넣어두기 때문에 나중에 처리할 수 있다. 여기서 기존 동기화 방식은 많은 메시지를 전송할 경우 병목 현상이 생길 수도 있고, 뒤에 들어오는 요청에 대한 응답이 지연될 것이다.

 

2. 낮은 결합도(Decoupling)

생산자 서비스와 소비자 서비스가 독립적으로 행하면서 서비스간 결합도가 낮아진다.

 

3. 확장성(Scalable)

생산자 서비스 혹은 소비자 서비스를 원하는대로 확장할 수 있기 때문에 확장성이 좋다.

 

4. 탄력성(Resilience)

소비자 서비스가 다운되더라도 어플리케이션이 중단되는 것이 아니라 메시지는 메시지 큐에 남아있다. 소비자 서비스가 다시 시작될 때마다 추가설정이나 작업을 수행하지 않아도 메시지 처리를 시작할 수 있다.

 

5. 보장성(Guarantees)

메시지 큐는 큐에 보관하는 모든 메시지가 결국 소비자 서비스에게 전달된다는 일반적인 보장을 제공한다.

 

메시지 큐의 오픈소스 시스템

- RabbitMQ

- Kafka

- ActiveMQ

 

 

 출처 

https://tecoble.techcourse.co.kr/post/2021-09-19-message-queue/

 

728x90
반응형
blog image

Written by ner.o

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

반응형

bool 변수 작명은 4가지로 한다.

  • is
  • has
  • 조동사
  • 동사원형

 

is 용법

가장 흔한 케이스이다. 보통 3가지 케이스로 나뉜다.

  • is + 명사
  • is + 현재진행형 (~ing)
  • is + 형용사

is + 명사

"(무엇)인가?" 라는 뜻으로 쓰인다.

boolean isStudent; // user는 학생인가?

is + 현재진행형 (~ing)

"~하는 중인가?" 라는 뜻으로 쓰인다.

boolean isExecuting; // 실행 중인가?
boolean isPending; // 대기 중인가?

is + 형용사 => 형용사 / ~ed(수동태)

boolean isSelected;
boolean isHidden;

 

has 용법

  • has + 명사 
  • has + 과거분사 (pp)

has + 명사

"~를 가지고 있는가?" 

boolean hasICloudAccount; // iCloud 계정을 가지고 있는가
boolean hasVideo; // video를 가지고 있는가?

has + 과거분사 => 현재완료

"과거에 완료된 것이 현재까지 유지가 되고 있다." "~가 유지되고 있는가?"

boolean hasConnected; // 연결되어 있는가?
boolean hasEnded; // 끝났는가?

 

조동사 + 동사원형

can / should / will 등이 있다.

can "~ 할 수 있는가?"

should, will "~ 해야하는 가?", "~할 것인가?"

boolean canEdit() { ... } // 편집할 수 있는가?

 

동사원형 => 3인칭단수로

예를 들어

  • supports : ~을 지원하는가?
  • includes : ~을 포함하는가?
  • shows : ~을 보여줄 것인가?
  • allows : ~을 허용할 것인가?
  • accepts : ~을 받아 주는가?
  • contains : ~을 포함하고 있는가?
boolean supportsVideo; // 비디오를 지원하는가?
boolean allowsEditing; // 편집을 허용하는가?

 

 

해당 사항은 Swift 언어의 bool 변수 작명(스위프트 디자인 가이드)으로 Java와 다른 부분이 있을수도 있지만, boolean 변수의 조동사를 넣어 자주 사용하는 필자로서, 그리고 Java와 비슷한 부분이 있어보여 정리할 필요가 있어보여 정리해보았다.

 

 출처 

https://soojin.ro/blog/naming-boolean-variables

 

Bool 변수 이름 제대로 짓기 위한 최소한의 영어 문법 · Soojin Ro

Background 프로그래머의 가장 어려운 업무가 이름 짓기라는 설문 결과도 있듯이 변수에 적절한 이름을 지어주는 것은 어렵고 오래걸리는 일이다. 영어가 모국어가 아닌 사람들에게는 더 어려울

soojin.ro

 

728x90
반응형
blog image

Written by ner.o

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

반응형

 이전 글 

https://frogand.tistory.com/209

 

[Java] Optional를 잘 사용하는 법 1 (NPE, if문으로 null 체크를 하지 말자)

Optional을 왜 사용하냐, 그럼 언제 사용해야할까, 에 대한 질문과 답을 적으려고 한다. 요점 - NPE을 방지하기 위해 사용하는 클래스. - null이 들어올 수 있는 값을 한 번 감싸는 Wrapper 클래스. NPE (Nul

frogand.tistory.com

Stream 처럼 사용하기

Optional을 제대로 사용하려면 최대 1개의 원소를 가지고 있는 특별한 Stream이라고 생각하는 것이 좋다. Optional 클래스와 Stream 클래스는 직접적인 구현, 상속과는 관련없지만 사용방식이 유사하다. Stream 클래스의 map(), flatMap(), filter() 와 같은 메서드가 Optional도 가지고 있기 때문에 Stream처럼 사용한다고 생각하면 될 것 같다.

 

map()

public String getCityOfMemberFromOrder(Order order) {
    if (order != null) {
        Member member = order.getMember();
        if (member != null) {
            Address address = member.getAddress();
            if (address != null) {
                String city = address.getCity();
                if (city != null) {
                    return city;
                }
            }
        }
    }

    return "Seoul"; // default
}

해당 코드를 Optional을 사용하여 변경하겠다.

public String getCityOfMemberFromOrder(Order order) {
    return Optional.ofNullable(order)
            .map(Order::getMember)
            .map(Member::getAddress)
            .map(Address::getCity)
            .orElse("Seoul");
}

이전 코드의 전통적인 NPE 방어 패턴에 비해 훨씬 간결하고 명확해진 코드를 볼 수 있다. 기존에 존재하던 조건문이 사라지고 Optional의 메서드 체이닝으로 대체되었다.

- ofNullable() 정적 팩토리 메서드를 활용하여 Order 객체를 Optional로 감쌌다. Order 객체가 null인 경우를 대비해 of() 메서드 대신에 ofNullable() 메서드를 사용했다.

- map() 메서드의 연쇄 호출을 통해 Optional 객체를 3번 변환하였다. 

Optional<Order> -> Optional<Member> -> Optional<Address> -> Optional<String>

- orElse() 메서드를 호출하여 기본값으로 사용할 값을 정해주었다.

 

filter()

null 체크로 시작하는 if 조건문 패턴이 많이 있다.

if (obj != null && obj.do() ...)

예를 들어, 주어진 시간(분) 내에 생성된 주문을 한 경우에만 해당 회원 정보를 구하는 메소드를 위 패턴으로 작성해보자.

public Member getMemberIfOrderWithin(Order order, int min) {
	if (order != null && order.getDate().getTime() > System.currentTimeMillis() - min * 1000) {
		return order.getMember();
	}
}

위 코드는 if 조건문에 null 체크와 비즈니스 로직이 같이 적혀있어서 가독성이 떨어진다.  

 

반면 filter() 메서드를 사용하면 if 조건문 없이 메서드 연쇄 호출만으로 가독성있는 코드를 작성할 수 있다. 또, 메서드의 리턴타입을 Optional로 사용함으로써 호출자에게 해당 메서드가 null을 담고있는 Optional로 반환할 수도 있다는 것을 명시적으로 알려주고 있다.

public Optional<Member> getMemberIfOrderWithin(Order order, int min) {
    return Optional.ofNullable(order)
            .filter(o -> o.gerDate().getTime() > System.currentTimeMillis() - min * 1000)
            .map(Order::getMember);
}

filter() 메서드는 넘어온 함수형 인자의 리턴 값이 false인 경우, Optional을 비우기 때문에 그 이후 메서드 호출이 의미가 없어진다.

 

Java8 이전 개발 코드를 Optional로 바꾸기

Java8 이전에 개발된 코드는 Optional이 없어 null-safe하지 않을 수 있다. JAVA 표준 API조차 하위 호환성을 보장하기 위해 기존 API에 Optional을 적용할 수 없었다.

 

메서드의 반환값이 존재하지 않을 때 전통적인 처리 패턴

이전 개발된 메서드들은 반환값이 존재하지 않을 경우 크게 2가지 패턴으로 처리하였다.

 

1. null 반환

Map 인터페이스의 get() 메서드는 주어진 인덱스에 해당하는 값이 없으면 null을 반환한다.

Map<Integer, String> cities = new HashMap<>();
cities.put(1, "Seoul");
cities.put(2, "Busan");
cities.put(3, "Daejeon");

보통은 null 체크 코드가 들어간다.

String city = cities.get(4); // returns null
int length = city == null ? 0 : city.length(); // null check
System.out.println("length: " + length);

get() 메서드의 반환값을 Optional로 감싸주면 null-safe하게 처리할 수 있다.

Optional<String> maybeCity = Optional.ofNullable(cities.get(4));
int length = maybeCity.map(String::length).orElse(0);
System.out.println("length: " + length);

map() 메서드로 결과를 얻어내고 orElse() 메서드로 디폴트 값을 설정해주었다.

 

2. 예외 발생

두번째 패턴은 예외를 던져버리는 경우이다. List 인터페이스의 get() 메서드는 주어진 인덱스에 해당하는 값이 없으면 ArrayIndexOutOfBoundsException 을 던진다.

List<String> cities = Arrays.asList("Seoul", "Busan", "Daejeon");

다음과 같이 try-catch 문을 사용하여 예외 처리를 해주고 예외 처리 이후에 null 체크도 진행해야 한다.

String city = null;
try {
    city = cities.get(3); // throws exception
} catch (ArrayIndexOutOfBoundsException e) {
    // 생략
}

int length = city == null ? 0 : city.length(); // null check
System.out.println("length: " + length);

이런 경우, 예외 처리부를 감싸서 정적 유틸리티 메서드로 분리해야 한다.

Optional 클래스의 정적 팩터리 메서드를 사용해서 정상처리와 예외 처리시에 반환할 Optional 객체를 각각 지정해주자.

public static <T> Optional<T> getAsOptional(List<T> list, int index) {
    try {
        return Optional.of(list.get(index));
    } catch (ArrayIndexOutOfBoundsException e) {
        return Optional.empty();
    }
}

Optional<String> maybeCity = getAsOptional(cities, 3); // Optional
int length = maybeCity.map(String::length).orElse(0); // null check
System.out.println("length: " + length);

 

ifPresent() 메서드 사용하기

ifPresent(Consumer<? super T> consumer

이 메서드는 특정 결과를 반환하는 대신에 Optional 객체가 감싸고 있는 값이 존재하는 경우에만 실행될 로직을 함수형 인자로 넘길 수 있다. 함수형 인자로 람다식이나 메서드 레퍼런스가 넘어올 수 있다.

Optional<String> maybeCity = getAsOptional(cities, 3); 
maybeCity.ifPresent(city -> {
	System.out.println("length: " + city.length());
});

 

728x90
반응형
blog image

Written by ner.o

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