반응형

1. 문제


프론트엔드 팀과 협업을 위해서 API 서버를 개발하고, AWS LightSail을 사용해서 서버를 배포했습니다.

클라이언트 서버가 띄워지기 전까지는 로컬에서 작업을 해서 몰랐지만, 띄우고 나서 확인해 보니 통신에 문제를 겪었습니다.

클라이언트 서버는 https로 배포가 되어있고, 개발 서버는 http로 배포가 되어 있어 두 서버 사이의 통신에 문제가 발생했습니다.

 

 

 

2. 해결


(1) SSL 인증서가 필요하다!

첫 번째로 HTTPS로 배포를 하기 위해서는 CA의 인증을 받은 SSL 인증서가 필요합니다. 

이 인증서는 보통 비용을 지불하고, 받아서 사용합니다. 하지만 저희는 돈이 없기 때문에!

무료로 SSL 인증서를 발급 받을 수 있는 Let's Encrypt를 사용했습니다.

무료라는 특징도 있지만 3개월(90일)마다 인증서를 갱신해줘야 한다는 특징도 존재합니다.

 

 

 

(2) 도메인 구매하기

letsencrypt에서 SSL 인증서를 받기 위해서는 "이메일"과 "도메인"이 필요합니다.

그래서 이 도메인이라는 것을 구매해야하는데, 가비아라는 사이트에서 도메인을 구매했습니다.

이렇게 사이트에서 원하는  도메인을 검색해보면 사용할 수 있는 도메인 목록이 나오게 됩니다.

이렇게 뜨면 원하는걸 골라서 선택한 후 신청하기를 진행하고, 결제를 하면 이제 제 도메인이 생깁니다.

위에는 20,000원짜리로 예시가 되어있는데 잘 찾아보면 500원짜리도 존재하고, 가격은 다양합니다.

자신이 적당하다고 생각되는 가격으로 도메인을 하나 구매해 주시면 됩니다.

이제 이렇게 도메인을 구매했으면 링크를 참고해서 미리 준비되어 있는 서버(AWS)의 Public IP를 DNS로 등록해줘야 합니다.

 

 

 

(3) My 가비아 > 도메인으로 접속

도메인을 구매하고, 조금 있으면 가비아 홈페이지의 오른쪽 상단에 My가비아에 들어가 보면 도메인을 관리할 수 있는 홈페이지로 갈 수 있습니다.

여기서 빨간색 박스에 있는 "도메인"을 클릭합니다.

들어가면 내가 구매한 도메인을 확인할 수 있고, "관리" 버튼을 클릭합니다.

쭉 내리다 보면 오른쪽 아래 DNS 레코드 설정으로 갈 수 있는 버튼이 있습니다. "설정"을 눌러줍니다.

내가 구매한 도메인을 확인하고, 체크하고 "DNS 설정"을 클릭합니다.

 

이제 "레코드 추가"를 누르고 호스트에 @와 www를 추가해줘야 합니다. 값/위치에는 AWS EC2 또는 다른 클라우드 서비스를 통해 띄운 서버의 IP를 입력해 줍니다.

 

오른쪽 수정 또는 확인 버튼을 눌러주고, "저장"을 눌러주면 DNS 서버에 내가 구매한 도메인과 내 서버의 IP가 연결이 완료되었습니다.

 

이렇게 등록하고 나서 http://poppleserver.store:8080 에 접속해 보면 http://{ubuntu ip}:8080에 접근했을 때와 같은 결과 페이지가 나오면 됩니다. 

DNS 서버의 쿼리가 퍼지기까지는 약간의 시간이 소요되고, 도메인 등록 업체에서는 12시간 정도를 얘기하고 있습니다. 하지만 실제로는 30분 정도 기다리면 사용하는데 문제가 없다고 합니다. 연결될 때까지 기다려 봅시다.

 

이렇게 연결이 되었으면 어떻게 확인해 볼까? 터미널을 켜고 nslookup 명령어로 확인해볼 수 있습니다.

nslookup 명령어 사용

NXDOMAIN 에러가 뜨면 DNS 서버에 제대로 등록될 때까지 조금만 여유를 가지고 기다려보시길 바랍니다!

그래도 해결이 안 된다면 방법을 찾아야 하겠지만, 제대로 된 IP에 등록하셨다면 될 겁니다!

 

 

 

(4) Certbot 설치

Ubuntu 서버로 접속한 후에 Certbot을 설치해줘야 합니다.

$ sudo apt-get update
$ sudo apt-get install certbot

저는 Ubuntu로 서버를 만들어서 apt-get을 사용했지만, 만약 Amazon Linux를 사용해서 서버를 띄우셨다면 yum을 사용해서 조금 복잡하게 설치를 했던 거 같습니다. (구글링 해보시면 좋을 거 같습니다!)

 

 

 

(5) 인증서 발급받기

$ sudo certbot certonly --standalone

standalone 옵션 말고 다른 옵션들도 있는데, 아래 링크에 첨부를 해뒀습니다.

저는 설정을 모두 끝마치고 서버를 다시 재시작할 것이기 때문에 standalone 옵션을 사용해서 발급했습니다.

 

위 명령어를 치면 이메일을 치라고 하고, 몇 가지 동의 사항을 받습니다. 본인의 이메일을 입력하고, yes를 입력해 주면 됩니다.

그러고 나서 마지막에 도메인을 입력하라고 합니다. 이때 저희가 구매한 도메인을 입력해 줍니다. (DNS 서버에 등록된 상태여야 합니다.)

 

입력하라고 하는 것을 모두 입력해 주면 LetsEncrypt에서 우리 도메인에 어떤 요청을 보내고, 제대로 연결된 도메인이 맞는지 확인하는 작업을 한 후 아래와 같은 문구가 뜹니다.

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/subbak2.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/subbak2.com/privkey.pem

 

💡 만약 계속해서 certification failed이 뜬다면 몇 가지를 확인해 보자!

(1) 우리 서버가 이미 80번 포트를 사용하고 있지는 않은가?
: LetsEncrypt는 80번 포트(http)로 GET 요청을 보내서 어떤 처리를 하고 나서 응답되는 결과가 기대한 결과여야 성공적으로 인증서를 발급해 준다. 하지만 80번 포트를 이미 누군가 쓰고 있으면 안 된다.

(2) 도메인이 DNS 서버에 잘 등록이 되었는가?
: LetsEncrypt가 이 도메인이 우리 서버와 잘 연결되었는지 확인하기 때문에 필수 조건이다. 이건 터미널에서도 확인이 가능하다. nslookup {도메인} 명령어를 사용해 보자.

 

 

 

(6) 인증을 통해 받은 파일로 Springboot 프로젝트로 가져갈 keystore 파일 생성하기

$ cd /etc/letsencrypt/live/subbak2.com

위 경로로 이동을 해봅시다. 위 경로는 인증 성공했을 때 나왔던 메시지의 경로입니다. 인증을 통해 만들어진 pem 파일이 있는 곳입니다.

 

저는 위 경로로 이동하려고 했을 때, live 파일에 대한 접근 권한이 없었습니다. 그래서 접근 권한을 풀어준 후 접근했습니다.

$ sudo chmod 777 live

이제 위 경로로 이동했다면 발급받은 pem 파일을 통해서 keystore 파일을 만들어 내야 합니다.

$ openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out keystore.p12 -name tomcat -CAfile chain.pem -caname root

코드는 길지만 그냥 Certbot을 통해서 발급받은 fullchain.pem과 privkey.pem을 이용해서 지지고 볶고 해서 Springboot에서 사용할 암호화된 keystore.p12 파일을 만든다는 명령어이다.

 

위 명령어를 입력하면 비밀번호를 치라고 하는 게 나오는데, 이때 치는 비밀번호는 나중에 application.yml에 넣어줘야 하는 비밀번호입니다. 아무거나 입력해도 되는데 기억하고 있어야 합니다.

 

생성된 keystore.p12를 Spring 프로젝트의 /src/main/resources로 가져가야 합니다. 이때는 리눅스의 mv 명령어로 가져가봅니다.

keystore.p12의 파일 권한이 없다면 권한을 허용하고, 이동을 해준다.

$ sudo chmod 777 keystore.p12
$ mv keystore.p12 {/src/main/resoureces 경로}

 

 

 

(7) application.yml 설정

#SSL
server:
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-type: PKCS12
    key-store-password: {keystore.p12 파일 만들 때 입력한 비밀번호}
    key-alias: tomcat

security:
  require-ssl: true

classpath:는 모두 알겠지만 /src/main/resources 경로를 나타냅니다.

 

아래 있는 security 설정은 Spring Security 때문에 해둔 설정인데, 추가하지 않아도... 되는 거 같은데 더 알아봐야 할 거 같습니다.

 

 

 

(8) 포트 포워딩 설정

이제 https://poppleserver.store로 요청을 하면 Spring 내장 톰캣의 8080으로 연결되도록 하고 싶습니다.

이때는 443 포트(HTTPS)로 들어온 요청을 8080 포트(톰캣)로 포트 포워딩을 해줍니다.

그럼 이제 https://poppleserver.store로 요청을 보내면 8080 포트로 포트 포워딩이 되어 실행됩니다.

$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8080

 

 

 

(9) https://{구매한 도메인}에 들어가서 API 요청 보내서 확인

위에 주소창 왼쪽에 자물쇠 모양이 생기고, 정상적으로 요청이 간다면 성공입니다!

주소창 자물쇠 확인

LetsEncrypt는 무료이지만, 단점이라고 하면 90일마다 인증서를 재발급해줘야 하는 귀찮음이 있습니다. 이 부분도 찾아보니 자동화를 하는 방법이 있었는데, 필요해질 때 적용해 보면 될 거 같습니다.

 

 

 

반응형
반응형

1. 문제


매일 00시 00분마다 작동되는 스케쥴러를 구현하기 위해서 다음과 같이 코드를 작성했습니다.

@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
@Transactional
public void surveyScheduling() {
    log.info("[{}] 스케쥴러 작동 설문조사 데이터 정리", LocalDateTime.now());

    LocalDate now = LocalDate.now();
    List<Survey> surveys = surveyRepository.findAllByStatusIsNot(SurveyStatus.REVERT);

    for (Survey survey : surveys) {
        if (isEnd(survey.getEndDate().toLocalDateTime(), now)) {
            survey.setStatus(SurveyStatus.REVERT);
        }
        if (isStart(survey.getStartDate().toLocalDateTime(), now)) {
            survey.setStatus(SurveyStatus.IN_PROGRESS);
        }
    }
}

private boolean isEnd(LocalDateTime endDate, LocalDate now) {
    return endDate.toLocalDate().isBefore(now);
}

private boolean isStart(LocalDateTime startDate, LocalDate now) {
    return startDate.toLocalDate().isEqual(now);
}

로컬에서 매일 00시 00분 정각에 작동되는 것을 테스트했습니다. (완전 무식하게 정각까지 안 자고 기다리고 있다가 확인했다. 😆)

 

그래서 정상적으로 작동되는 것을 확인하고, AWS EC2 서버에 변경된 코드를 적용해서 배포했습니다.

그리고 하루를 기다렸다가 스케쥴러가 작동 했는지 확인하려고 데이터베이스를 확인해 봤습니다.

음..? 저 빨간색 네모 안에 있는 status 값이 분명 WAIT에서 IN_PROGRESS로 변경되었어야 하는데... 왜 변하지 않았을까...?

 

스케쥴러가 작동 안 됐나 하면서 AWS 서버에 접근해서 로그 기록을 확인해 봤습니다. 분명 스케쥴러가 작동했다면 INFO 레벨의 로그가 찍혔을 것이기 때문에...

분명 INFO 레벨의 지정해 둔 로그가 찍혔다. 그럼 정상적으로 스케쥴러가 작동했다는 것을 의미한다. 근데 제가 의도한 시간이었던 2023년 8월 4일 00:00:00이 아닌 2023년 8월 3일 15:00:00에 동작을 하는 문제가 발견되었습니다.

 

 

2. 문제 분석


처음에 AWS EC2 인스턴스를 만들 때 Asia/Seoul로 선택해서 만들어서 당연히 동작될 줄 알았습니다. 그래서 일단 AWS 서버에 접근해서 date 명령어로 현재 서버의 시간을 한번 확인해 봤습니다.

현재 제 컴퓨터에서는 시간이 2023년 8월 4일 19:05인데, 서버에서는 2023년 8월 4일 10:04로 찍혔습니다. AWS 서버와 현재 제 로컬의 시간이 다르다는 것을 확인했습니다. 

 

UTC라는 세계 표준 시간을 사용해서 현재 한국의 시간에서 9시간을 빼면 AWS EC2의 서버 시간이 된다고 합니다. 허허허....

 

그렇게 생각을 하니깐 위에서 8월 3일 15:00에 작동한 게 이해가 됐습니다. 한국 시간으로는 8월 4일 00:00에서 9시간을 빼면 8월 3일 15:00가 나오기 때문이었습니다.

 

 

 

3. 해결 과정


(1) 서버 시간에서 그럼 9시간을 더해서 재설정해두면 되지 않을까?

되게 단순한 발상인데, 나름 그럴싸한 해결방법 같아 보입니다. 바로 실행에 옮기기 전에 항상 먼저 구글링을 시작합니다.

(터미널에서 뭐 잘못 건드렸다가 노트북 싹 다 날려버렸던 경험이 있어서.... 항상 제대로 알고 나서 실행하는 편...)

 

>> sudo date -s "2023-08-04 19:20:00"

위 명령어로 서버의 시간을 재설정할 수 있다고 한다. 근데 역시 위험한 느낌의 직감은 틀리지 않습니다.

위에 있는 명령어로 시간을 재설정하게 되면 EC2 타임존이 기본적으로 UTC로 되어 있어서 나머지 근본적인 지역에 대한 설정은 하나도 안 바뀌고 날짜만 한국 날짜로 바뀐 거여서 다른 서비스와 시간 동기를 맞출 때 시간이 다르다고 판단되어 의도하지 않은 오류가 발생할 수 있다고 합니다.

 

 

(2) AWS 서버의 타임존을 한국으로 바꿔보자.

첫 번째 방법처럼 날짜만 한국에 맞추는 것이 아니고, 근본적인 타임존을 한국으로 한번 설정해 봅시다.

>> sudo cat /etc/localtime

위 명령어로 한번 확인을 해보면 현재는 UTC0로 쓰여있습니다. 즉, 현재는 UTC 기준으로 작동된다는 것을 의미합니다.

이 부분을 이제 한국의 타임존을 쓸 수 있도록 수정해 봅시다.

 

>> sudo rm /etc/localtime
>> sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

기존에 있는 UTC 타임존 파일을 제거하고, 서울 기준의 타임존 파일을 새롭게 적용하는 명령어입니다.

 

위 명령어로 재설정을 하고, 다시 date 명령어로 서버의 시간을 확인해 봅시다.

이제 시간이 의도한 대로 한국 시간으로 설정이 되었습니다.

이제 데이터베이스의 startDate를 2023년 8월 5일로 바꾸고, 하루를 기다려보겠습니다.

 

이제 2023년 8월 5일 00:00이 되면 저 id 1번의 데이터에서 status 값이 WAIT에서 IN_PROGRESS가 되어야 합니다. 제발 돼라...

 

데이터베이스를 확인해보니 정상적으로 스케쥴러가 2023년 8월 5일 00:00에 동작을 한 거 같네요! 😆

이제 AWS 서버에도 INFO 로그가 찍혔는지도 한번 확인해보겠습니다.

 

음...동작은 해서 update 쿼리가 실행되어 WAIT 상태를 IN_PROGRESS로 변경시키기는 했지만 여전히 로그에 찍힌 시간을 보면 UTC 기준으로 15:00:00이라고 찍히는 것을 확인할 수 있습니다.

 

 

(3) 프로젝트 기본 TimeZone 변경하기 + 로그에 찍히는 시간도 변경

@SpringBootApplication
public class ServerApplication {

    public static void main(String[] args) {

        SpringApplication.run(ServerApplication.class, args);
    }

    @PostConstruct
    public void init() {
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
        System.out.println(">>> 현재 시간은 " + LocalDateTime.now());
    }

}

@PostConstruct를 사용해서 Spring 프로젝트의 기본 TimeZone 세팅을 "Asia/Seoul"로 지정하도록 했습니다.

그리고 제대로 동작하는지 확인하기 위해서 LocalDateTime.now()를 사용해서 콘솔에 흔적을 남기도록 하고 한번 동작해 봤습니다.

빨간색을 보면 정상적으로 "Asia/Seoul"의 시간이 찍힌 것을 확인할 수 있었습니다. (현재 노트북 시간과 동일)

하지만 파란색 부분을 보면 로그로 찍히는 시간은 여전히 UTC 기준으로 찍히는 것을 볼 수 있습니다.

이 부분도 한번 바꿔봅시다! 간단하게 application.yml 파일을 설정해 주면 됩니다.

logging:
  pattern:
    dateformat: yyyy-MM-dd HH:mm:ss.SSSz,Asia/Seoul

Log에 찍히는 시간의 dateformat을 설정해 줄 수 있습니다.

이때 Timezone은 당연히 Asia/Seoul로 해주면 됩니다.

이제 다시 서버에 jar 파일을 올려서 실행시켜서 확인해 봅시다.

아주 완벽하게 LocalDateTime.now()도 현재 시간으로 뜨고, 로그에 찍히는 시간도 현재 시간으로 찍히는 것을 확인할 수 있습니다!

 

이제 아래와 같이 스케쥴러 코드를 살짝만 수정해서 10초가 될 때마다 작동되는 스케쥴러를 작동시켜 보겠습니다.

@Scheduled(cron = "10 * * * * *", zone = "Asia/Seoul")
@Transactional
public void surveyScheduling() {
    log.info("[{}] 스케쥴러 작동 설문조사 데이터 정리", LocalDateTime.now());

    LocalDate now = LocalDate.now();
    List<Survey> surveys = surveyRepository.findAllByStatusIsNot(SurveyStatus.REVERT);

    for (Survey survey : surveys) {
        if (isEnd(survey.getEndDate().toLocalDateTime(), now)) {
            survey.setStatus(SurveyStatus.REVERT);
        }
        if (isStart(survey.getStartDate().toLocalDateTime(), now)) {
            survey.setStatus(SurveyStatus.IN_PROGRESS);
        }
    }
}

실행 결과를 확인해 보면 로그에 찍히는 시간과 작동할 때 LocalDateTime.now()로 찍은 시간이 동일한 것을 확인했습니다. 🙆🏻‍♂️

다시 코드를 원복해서 서버에 적용해둡시다!

 

반응형
반응형

HTTPS는 애플리케이션 계층과 전송 계층 사이에 신뢰 계층인  SSL/TLS 계층을 넣은 신뢰할 수 있는 HTTP 요청을 말합니다.

 

1. SSL/TLS (Secure Socket Layer, Transport Layer Security Protocol)


  • SSL은 1 버전부터 시작해서 2, 3 그리고 TLS 1.0, TLS 1.3까지 버전이 업그레이드되며 마지막으로 TLS로 명칭이 변경되었지만, 보통 SSL/TLS로 불립니다.
  • SSL/TLS는 전송 계층에서 보안을 제공하는 프로토콜입니다. 클라이언트와 서버가 통신할 때 이 프로토콜을 통해서 제3자가 메시지에 접근할 수 없도록 해줍니다. (인터셉터 방지)
  • SSL/TLS는 보안 세션을 기반으로 데이터를 암호화하며 보안 세션이 만들어질 때 인증 메커니즘, 키 교환 암호화 알고리즘, 해싱 알고리즘이 사용됩니다.
보안 세션
보안이 시작되고 끝나는 동안 유지되는 세션을 의미합니다. SSL/TLS는 Handshake를 통해서 보안 세션을 생성하고, 이를 기반으로 상태 정보 등을 공유합니다.

 

 

 

2. HTTPS를 사용하는 이유


  • HTTP 프로토콜은 암호화되지 않은 평문의 데이터를 전송하는 프로토콜이기 때문에 내가 서버로 보낸 정보(id, password)를 중간에 누군가 훔쳐볼 수 있습니다.
  • HTTPS를 사용하게 되면 클라이언트와 서버만 알고 있는 형태로 암호화되어 데이터를 전송하기 때문에 중간에 훔쳐보더라도 어떤 정보인지 알아내기 힘듭니다.
  • 그리고 접속한 사이트가 신뢰할 수 있는 곳인지를 사용자에게 알려줍니다.
  • 즉 HTTP에서 보안적인 부분까지 포함된 것이 HTTPS이기 때문에 대부분의 서비스에서는 현재 HTTPS 프로토콜을 사용합니다.

 

 

 

3. 대칭키와 비대칭키


  • HTTPS의 보안 방식을 알아보기 위해서는 대칭키와 비대칭키에 대해서 알고 있어야 합니다.

 

(1) 대칭키

  • 서버와 클라이언트 양쪽이 모두 동일한 키로 데이터를 복호화하는 방법을 말합니다.
  • 데이터를 전송할 때는 키를 가지고 데이터를 암호화해서 보내고, 받는 쪽에서 동일한 키를 가지고 데이터를 복호화해서 사용합니다.
  • 하지만 서버와 클라이언트 모두 동일한 키를 가지기 위해서는 한 번은 키에 대한 정보도 보내줘야 하는데, 키를 전송하는 과정에서 누군가에게 키가 노출된다면 데이터를 암호화하는 이유가 사라지게 된다는 한계가 있습니다.
  • 그래서 나온 방법이 비대칭키입니다.

 

 

(2) 비대칭키 (공개키)

  • 개인키와 공개키를 한 쌍으로 사용하는 암호화/복호화 방법입니다.
  • 서버와 클라이언트가 있으면 서버에서는 개인키(나만 알고 있는 키)를 가지고 있고, 클라이언트들은 공개키(모두가 볼 수 있는 키)를 가질 수 있습니다.
  • 내 개인키로 암호화한 데이터는 같은 쌍의 공개키로만 복호화를 할 수 있고, 반대로 공개키로 암호화한 데이터는 같은 쌍의 개인키로만 복호화를 할 수 있습니다.
  • 이 방법을 사용하면 대칭키의 한계를 해결할 수 있습니다. 클라이언트가 데이터(id, password)를 공개키로 암호화해서 보내더라도 훔쳐보는 사람이 같은 쌍의 개인키를 가지고 있지 않으면 절대 내용을 볼 수 없습니다.
  • 반대로 신뢰할 수 있는 서버에서 보내준 사이트라는 것을 구분할 수 있는 원리는 다음과 같습니다.

신뢰할 수 있는 사이트인지 알 수 있는 방법

 

 

 

(3) 클라이언트와 서버의 HTTPS를 통한 데이터 전달

(3.1) 클라이언트는 처음에 서버를 신뢰하지 못하기 때문에 둘 사이에 Handshake를 거칩니다.

  • 클라이언트가 랜덤 데이터를 생성해서 서버로 보냅니다.
  • 서버는 클라이언트에게 전달받은 데이터에 추가로 서버가 랜덤으로 생성한 어떤 데이터와 해당 서버가 발급받은 인증서를 실어서 다시 클라이언트에게 보냅니다.

1번 과정

 


(3.2) 클라이언트는 서버에서 받은 데이터(인증서)를 비대칭키 방식을 사용해서 복호화합니다.

  • 클라이언트는 전달받은 데이터(인증서)를 브라우저에 내장되어 있는 CA를 통해서 정보를 확인하게 됩니다.
  • CA의 인증을 받은 인증서는 CA의 개인키로 암호화가 되어 있습니다. 즉, 클라이언트는 서버 쪽에서 보내준 인증서가 브라우저에 내장된 CA가 인증한 신뢰할 수 있는 인증서인지를 확인할 수 있습니다.
  • 클라이언트는 브라우저에 저장된 CA의 공개키를 가지고 해당 인증서를 복호화해 볼 수 있습니다. 복호화가 된다면 신뢰할 수 있는 서버가 보낸 데이터이고, CA 리스트에 있는 모든 공개키로 인증서가 복호화되지 않는다면 신뢰할 수 없는 서버에서 보낸 데이터입니다.
  • 신뢰할 수 있는 서버에서 보낸 인증서를 CA의 공개키로 복호화해 보면 그 안에는 해당 서버의 공개키가 포함되어 있습니다.

2번 과정

 

(3.3) 클라이언트와 서버는 대칭키 방식으로 데이터를 주고받습니다.

  • 주고받는 다량의 데이터들을 계속 비대칭키 방식으로 암호화, 복호화하는 것은 컴퓨터에 큰 무리가 될 수 있는 작업입니다.
  • 그래서 1번 과정인 Handshake 과정에서 주고받은 클라이언트가 생성한 랜덤한 데이터와 서버가 생성한 랜덤한 데이터를 가지고 클라이언트는 임시키를 만들어냅니다.
  • 이렇게 만들어낸 임시키를 2번 과정에서 얻어낸 해당 서버의 공개키를 사용해서 암호화하여 서버로 보낸 후 양쪽(클라이언트, 서버)에서 일련의 과정을 거쳐서 둘만 가지고 있는 동일한 대칭키를 만들어냅니다.
  • 이때부터는 이제 이 대칭키를 사용해서 데이터를 암호화하고 복호화해서 전송해도 제3자가 이 내용을 알아볼 수 있는 방법은 없습니다. 

클라이언트 -> 서버로 임시키 전달
일련의 작업을 거쳐 동일 대칭키를 클라이언트와 서버가 갖게됨

CA (Certificate Authorites)
공인된 민간 인증 기관들을 말합니다. CA는 신뢰성이 엄격하게 공인된 기업들만 참여할 수 있습니다. 클라이언트가 사용하는 크롬, 사파리, 엣지 등 브라우저에는 이 CA들의 목록이 내장되어 있습니다. 대표적인 CA는 Comodo, GoDaddy, GlobalSign, 아마존이 있습니다.

 

 

 

4. HTTP/3


  • TCP 위에서 돌아가는 HTTP/2와는 다르게 HTTP/3는 UDP 기반의 QUIC(Quick UDP Internet Connection) 프로토콜을 사용합니다.
  • HTTP/2에서 HTTP/1.1의 문제점을 개선하기 위해 만들었던 멀티플렉싱을 HTTP/3에서 모두 가지고 있습니다.
  • QUIC는 TCP를 사용하지 않기 때문에 통신이 시작될 때 진행하는 3-way-handshake 과정을 거치지 않아도 되기 때문에 초기 연결 설정 시에 지연 시간이 감소되는 장점을 가지고 있습니다.
  • QUIC는 첫 연결 설정에서 클라이언트가 서버에 어떤 신호를 한 번 주고, 서버도 거기에 응답하기만 하면 바로 통신을 시작할 수 있습니다.
  • 추가로 QUIC는 순방향 오류 수정(Forward Error Correction) 메커니즘을 가지고 있기 때문에 전송한 패킷이 손실되면 수신 측에서 에러를 검출하여 수정하는 방식을 사용해서 패킷 손실률을 많이 낮추고 있습니다. (UDP의 단점 보완)

 

HTTP/2와 HTTP/3의 계층 비교

  • QUIC에는 TLS 1.3 기반의 내장 암호화 프로토콜이 포함되어 있어서 보안 통신을 제공하고, 제3자가 인터넷 트래픽을 가로채고 조작하는 것을 어렵게 만들어줍니다.

 

 

💡 나올 수 있는 면접 질문

  1. 암호화방식 중 대칭키 방식과 비대칭키 방식에 대해서 설명해 보세요.
  2. HTTP 통신 방식의 문제점에 대해서 설명해 보세요.
반응형
반응형

저번에 9주 차 회고를 작성했었는데, 벌써 5주가 지나서 14주 차가 되었습니다. 허허 가볍게 회고록을 작성해보려고 합니다.

 

 

🙋🏻‍♂️ Database/Spring 실시간 강의 시작

저번 Java 수업이 끝나고 이제 Database와 Spring 실시간 강의를 시작했습니다. 혼자서 Spring 공부해 볼 때 최주호 강사님이 운영하는 메타코딩이라는 유튜브 채널에서 공부를 한 적이 있었는데, 이번 Spring 실시간 강의 강사님으로 오셔서 약간 기대가 되었습니다.

 

지금 벌써 강의를 시작한지 몇 주가 지났는데, 개인적으로 실시간 강의 만족도는 매우 높습니다. 모르고 넘어갈만한 내용들을 자세하게 알려주셨습니다. 특히 그림으로 그리면서 적절한 예시를 항상 들어서 설명해 주시는데 이 부분이 만족도가 굉장히 높았습니다.

 

Database, JDBC, MyBatis 강의를 지나서 지금은 Hibernate와 JPA에 대해서 강의를 진행중인데, 다음 주에는 두 번째 토이 프로젝트가 예정이 되어 있습니다. 간단한 프로젝트이긴 한데 이번에는 2인 1조로 프로젝트가 진행돼서 약간 기대반 설렘반입니다.

 

 

 

🥇 백준 골드 달성

저번 회고 글에서 다음 회고 글에서는 골드가 되어있었으면 좋겠다고 했었는데, 골드를 달성했습니다. 알고리즘 문제는 빠짐없이 매일 1문제씩 백준에서 풀고 있습니다.

골드 구간으로 넘어오고 나서 요즘은 삼성 기출 코딩테스트 구현 문제를 하나씩 건들고 있는데, 확실히 난이도가 높아지고 있는 것을 느끼고 있습니다. 다음 회고 글에서는 플래티넘이 되었으면 좋겠다고 쓰고 싶지만 경험치도 엄청 천천히 오르고 문제는 어려워져서 힘들 거 같습니다. 하지만 목표로 잡은 플래티넘이 되는 날까지 꾸준히 풀어갈 계획입니다.

 

 

 

🧑🏻‍💻 사이드 프로젝트 계획

이 부분은 정규 커리큘럼은 아니지만 팀원 분들과 의견이 맞아서 계획 중에 있습니다. 처음에는 Spring MVC를 공부하면서 Springboot와 Thymeleaf로 백엔드 팀원들끼리만 프로젝트를 진행해보려고 했었는데, 계획을 하다보니 프론트 팀원분들을 모집하게 되었습니다.

 

완성도 높은 프로젝트를 해보면 좋겠지만, 지금은 앞으로 있을 미니 프로젝트와 파이널 프로젝트를 대비하기 위한 간단한 연습정도로 생각하고 참여해볼 생각입니다. 그렇다고 느슨하게 할 생각은 없습니다. 프로젝트는 프론트 팀원분들이 확정되면 기획을 마무리하고 바로 개발에 들어가 볼 계획을 가지고 있습니다. 다음 회고글에서는 사이드 프로젝트 결과에 대해서 다루고 있을 수도 있겠네요.

반응형
반응형

잘못 올라간 .idea 폴더가 존재

처음에 .gitignore 설정을 잘못해 줘서 쓸모없는 .idea 폴더가 올라갔다. 😅

 

 

(1) 터미널에 명령어 작성


git rm --cached -r .idea
  • 원격 저장소(깃허브)에 저장되어 있는 .idea를 삭제할 수 있는 명령어입니다.
  • 로컬 저장소에 있는 .idea은 삭제되지 않습니다.

 

 

(2) 삭제 상태 확인해보기


git rm --cached -r .idea 명령어 후에 git status 결과

  • 원격 저장소에서 삭제를 원하는 .idea을 삭제했습니다.
  • 이제 이 변경 사항을 Commit으로 작성해서 Push 해주면 됩니다.

 

 

(3) Commit 작성 후 Push


  • git commit 명령어를 통해서 커밋 메시지를 작성한 후 origin 저장소에 push 했습니다.

 

 

(4) 원격 저장소 확인


.idea 폴더가 원격 저장소에서 사라진 모습

  • .idea 폴더가 정상적으로 삭제된 것을 확인해 볼 수 있습니다.
반응형
반응형

1. 문제


Hibernate를 사용해서 간단하게 Customer 객체를 디비에 저장하는 save()와 모든 유저를 조회하는 findAll() 그리고, 단일 조회할 수 있는 findById()를 만들어서 테스트 중에 있었다.

 

@Import(CustomerRepository.class)
@DataJpaTest
class CustomerRepositoryTest {

    @Autowired
    private CustomerRepository customerRepository;

    @Test
    public void findById_test() {
        // Given
        Customer customer = Customer.builder()
                .name("코스")
                .tel("0101111")
                .build();
        customerRepository.save(customer);
        Long id = 1L;

        // When
        Customer customerPS = customerRepository.findById(id);

        // Then
        assertThat(customerPS.getName()).isEqualTo("코스");
    }

    @Test
    public void save_test() {
        // Given
        Customer customer1 = Customer.builder()
                .name("코스")
                .tel("0101111")
                .build();
        Customer customer2 = Customer.builder()
                .name("홍길동")
                .tel("0102222")
                .build();

        // When
        customerRepository.save(customer1);
        customerRepository.save(customer2);

        // Then
        Customer customerPS = customerRepository.findById(1L);
        System.out.println(customerPS);
        assertThat(customerPS.getName()).isEqualTo("코스");
    }

    @Test
    public void findAll() {
        // Given
        Customer customer1 = Customer.builder()
                .name("코스")
                .tel("0101111")
                .build();
        Customer customer2 = Customer.builder()
                .name("임꺽정")
                .tel("0101111")
                .build();
        customerRepository.save(customer1);
        customerRepository.save(customer2);

        // When
        List<Customer> result = customerRepository.findAll();

        // Then
        assertThat(result.size()).isEqualTo(2);
        assertThat(result.get(1).getName()).isEqualTo("임꺽정");
    }
}

테스트를 하나하나 실행시켰을 때는 통과가 나왔지만, 전체 테스트를 돌려보니 계속 실패가 나왔다.

내가 처음에 생각한 로직은 다음과 같다.

 

  1. 테스트 메서드가 하나씩 실행되고 끝날 때 Transaction이 종료되고, Rollback을 해준다고 알고 있다. 이건 @DataJpaTest에 @Transactional이 붙어있기 때문이다.
  2. 그럼 각 메소드가 종료될 때 Rollback이 되니깐 Id(PK)도 당연히 다시 1부터 시작된다고 생각해서 모든 테스트를 같이 돌려도 완벽히 격리된 테스트를 할 수 있을 거라고 생각했다.

 

 

 

2. 분석


궁금해서 @AfterEach와 @BeforeEach를 사용해서 각 테스트의 시작 전과 끝난 후에 findAll() 결과를 출력해 봤다.

@BeforeEach
public void beforeCheck() {
    System.out.println("===== Before Check =====");
    check();
    System.out.println("=======================");
}

@AfterEach
public void afterCheck() {
    System.out.println("===== After Check =====");
    check();
    System.out.println("=======================");
}

public void check() {
    List<Customer> result = customerRepository.findAll();
    if (result.isEmpty()) {
        System.out.println("비어있습니다.");
    } else {
        result.forEach(customer -> {
            System.out.println(">>> " + customer.getId() + " " + customer.getName() + " " + customer.getTel());
        });
    }
}
# findById 테스트

===== Before check =====
비어있습니다.
=======================

===== After check =====
>>> 1 코스 0101111
=======================


# findAll 테스트

===== Before check =====
비어있습니다.
=======================

===== After check =====
>>> 2 코스 0101111
>>> 3 임꺽정 0101111
=======================


# save 테스트

===== Before check =====
비어있습니다.
=======================

===== After Check =====
>>> 4 코스 0101111
>>> 5 홍길동 0102222
=======================

출력 결과를 보면 다음 테스트가 시작되기 전에 이전 데이터는 Rollback이 되는 게 맞다. 근데 보면 Id 값은 1로 초기화되지 않는 것을 확인해 볼 수 있다.

 

혹시나 현재 h2 데이터베이스를 사용하고 있어서 그런 건가 하고 MySQL로 연동해서 동일하게 실행해 보니 동일한 결과가 나왔습니다.

 

이후에 조금 궁금해져서 MySQL을 실행해서 직접 테이블을 하나 만들고, 트랜잭션을 만들어서 Insert 후 동일하게 Rollback을 해봤습니다.

# 테이블 생성
create table member (
	id bigint not null auto_increment,
    name varchar(20),
    primary key(id)
}

# 트랜잭션 시작
start transaction;

insert into member(name) values('오징어');
insert into member(name) values('김징어');

Rollback 전 Select

# 롤백 후 커밋
rollback;
commit;

Rollback 후 Select

# 다시 데이터 삽입
insert into member(name) values('이징어');

데이터 추가 삽입 후 Select

테스트 코드 상황과 동일하게 상황을 구성해서 직접 쿼리를 작성해봤는데, 롤백 후에도 Id가 1부터 시작되는 것이 아닌 3부터 시작돼서 저장되는 것을 확인해 볼 수 있었습니다.

 

 

 

3. 해결 방법


@AfterEach에서 각 테스트가 끝나면 Id의 시작점을 RESTART 해준다.

@AfterEach
public void check() {
    em.createNativeQuery("ALTER TABLE customer ALTER COLUMN id RESTART")
            .executeUpdate();
}

이 방법은 솔직히 좋은 방법은 아니라고 생각합니다. NativeQuery를 이렇게 사용해서 한다는 것은 특정 DB에 종속적인 테스트를 하고 있다는 것을 의미합니다.

 

사실 계속 방법을 찾다가 현재 작성한 테스트를 한 번에 실행했을 때 통과시켜보고 싶다는 생각에 찾아본 방법입니다.

 

전체 테스트 실행 결과

 

 

 

4. 느낀 점


원하는 결과는 얻었지만, 좋지 않은 테스트라는 것은 분명하기 때문에 더 좋은 방법을 찾아봐야 할 거 같습니다.

테스트를 할 때는 Id 값을 저렇게 Magic Number를 사용해서 하는 방법도 좋지 않은 것 같습니다.

테스트에 관해서는 더 찾아보고, 학습해야될 요소라고 생각됩니다. 단순히 궁금점이 조금 생겨서 기초부터 파고 들어봤던 거 같습니다.

반응형

+ Recent posts