Spring, DI, OOP
Spring, MVC
Mysql Jdbc Driver
만 사용해서 작성했던 DAO 클래스들에 JdbcTemplate
를 적용했다.queryForObject()
로 DB에서 값을 조회할 때, 결괏값의 개수가 0개이면 EmptyResultDataAccessException
가 발생해, try ~ catch
문으로 직접 예외를 처리해야 하는 부분이 아쉬웠다.JDBC
A.
jdbcTemplate을 이용해서 select쿼리를 날리는 경우 .query 메소드를 사용하게 된다.
그런데 .query 메소드의 경우 List형태로 리턴을 해주기 때문에 .queryForObject 메소드를 사용하는 경우도 있다.
다만 .queryForObject 메소드의 경우에는, row가 정상적으로 1개 나왔을 경우에는 문제가 없는데, row가 아예 나오지 않는 경우에 대해 exception을 떨구게 된다.
이곳 저곳 구글링을 해보면, try catch 문으로 exception을 캐치하여 null을 리턴하도록 하라고는 하는데, try catch 문의 경우 성능 저하의 원인이 될 수 있기 때문에 웬만해선 사용을 권장하지 않는다.
해결방안은 .query메소드를 사용하되, 단일 행이 나올 것이라고 예상되는 구문에서는 DataAccessUtils.singleResult나 DataAccessUtils.uniqueResult를 이용하여 List형태를 Object형태로 리턴하게끔 하면 된다.
메소드 구현체 내부를 들어가보면 알겠지만, .query 메소드의 결과로 나온 List의 사이즈를 체크하여, 사이즈가 0이면 null을 리턴하고, 사이즈가 0이 아니면 첫번재 element를 끄집어와서 리턴을 해주는 방식으로 되어 있다.
출처: https://hakurei.tistory.com/15 [Reimu's Development Blog]
OOP, Domain, Test, Spring
Test, DB
Spring, MVC, Exception
@ExceptionHandler를 등록한 Controller에만 적용된다. 다른 Controller에서 명시되어있는 예외가 발생하더라도 예외를 처리할 수 없다.
@ExceptionHandler가 하나의 클래스에 대한 것이라면, @ControllerAdvice는 모든 @Controller 즉, 전역에서 발생할 수 있는 예외를 잡아 처리해주는 annotation이다.
@ControllerAdvice와 동일한 역할 즉, 모든 Controller에서 발생하는 예외를 잡아 핸들링 하는 기능을 수행하면서 @ResponseBody를 통해 객체를 리턴할 수도 있다는 얘기다.
ViewResolver를 통해서 예외 처리 페이지로 리다이렉트 시키려면 @ControllerAdvice만 써도 되고, API서버여서 에러 응답으로 객체를 리턴해야한다면 @ResponseBody 어노테이션이 추가된 @RestControllerAdvice를 적용하면 되는 것이다.
@RestController에서 예외가 발생하든 @Controller에서 예외가 발생하든 @ControllerAdvice + @ExceptionHandler 조합으로 다 캐치할 수 있고 @ResponseBody의 필요 여부에 따라 적용하면 된다는 것이다.
@ControllerAdvice + @ResponseBody = @RestControllerAdvice
출처: https://jeong-pro.tistory.com/195 [기본기를 쌓는 정아마추어 코딩블로그]
HTTP, REST, API
HTTP, Cookie
보안, Hash
운영, 보안
A.
일반 서비스 접속용 경로와 별도로 관리자용 접속 경로인 Bastion 서버를 구성했습니다.
이를 통해 서비스 접속 경로의 책임과, 관리자 접속 경로의 책임을 분리했습니다.
모든 서버에 동일한 수준의 보안을 설정하면, Auto-Scaling 등의 확장을 위한 관리 포인트가 늘어나, 일반적으로 보안의 일정 부분을 포기하는 결정을 하게 됩니다.
Bastion 서버를 두면, 이러한 보안 문제와 확장성 문제를 모두 해결할 수 있습니다.
Bastion 서버가 공격을 받았을 때, 이 부분만 재구성하면 되므로 서비스 피해를 최소화 할 수 있습니다.
DDos 공격을 받고 있다면 일반 서비스 접속 경로로 접속 할 수 없습니다. 이때, 관리자용 접속 경로인 Bastion 서버를 구성해 두었다면 이를 통해 접속할 수 있습니다.
운영, Thread
A thread dump is a snapshot of the state of all the threads of a Java process.
Thread Dump 란, Java 프로세스의 모든 Thread들의 스냅샷(상태를 사진처럼 찍은 것)을 말한다.
In computer systems, a snapshot is the state of a system at a particular point in time.
Snapshot 이란, 특정 시점의 시스템 상태를 말한다.
Snapshot 이란, 특정 시점의 시스템 상태를 사진처럼 찍은 것을 말한다.
A.
Thread Dump란, Java 프로세스의 모든 Thread들의 스냅샷을 말합니다.
스냅샷이란, 특정 시점의 시스템 상태를 말합니다.
즉 Thread Dump란, Java 프로세스의 모든 Thread들의 특정 시점의 상태를 사진처럼 찍은 것을 말합니다.
Log
운영, AWS, CloudWatch
배포, Container, Docker
Test, ATDD
A.
ATDD는 인수 테스트를 먼저 작성한 다음, 기능 개발을 하는 방식입니다.
인수 테스트는 시스템의 인수 결정을 위해 사용자 입장에서 요구사항을 테스트하는 것을 말합니다.
블랙박스 테스트로, 시스템 내부 구현은 모르는 상태로 API 요청과 응답으로만 테스트합니다.
인증, JWT
A.
세션을 사용하는 방식은 Stateful한 방식입니다.
인증 상태를 서버 메모리나 데이터베이스에 보관하고 유지합니다.
이 방식은, 로그인 중인 사용자가 많아질수록 서버에 과부하가 생깁니다.
서버 확장을 해야할 때, 인증 상태 유지와 호환을 고려해야 하기 때문에 Stateful한 세션 방식은 서버 확장에 불리합니다.
세션의 기반이 되는 쿠키는 단일 도메인 및 서브 도메인에서만 작동하도록 설계되어있어, CORS 이슈를 관리해주어야 하는 번거로움이 있습니다.
토큰 기반 인증 시스템은 Stateless한 방식입니다.
서버가 인증 상태를 저장하지 않습니다.
클라이언트 요청에 포함되어있는 인증 토큰만을 가지고 인증에 대한 정보를 얻고 처리합니다.
이렇게 상태가 없는 경우 클라이언트와 서버간의 연결고리가 없기 때문에 서버의 확장성(Scalability)이 높아집니다.
JWT는 토큰 기반 인증 시스템의 구현체 이며, 웹 표준입니다.
JSON 데이터 포맷을 기반으로 한 웹 토큰입니다.
모든 정보를 자체적으로 지니고 있습니다.
HTTP 헤더에 넣어서 전달할 수 있고, URL의 파라미터로 전달할 수도 있습니다.
인코딩 : 데이터의 표준화, 보안, 처리속도 향상, 저장 공간 절약 등을 위해 다른 형태로 변환하는 방식을 말합니다.
이미지, 동영상, 오디오 전송에 많이 활용됩니다.
인코딩의 반대는 디코딩입니다.
Base64 : 8비트 이진 데이터(실행파일, ZIP파일 등)를 문자코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로 바꾸는 인코딩 방식을 말합니다.
6bit당 2bit의 오버헤드가 발생해, 전송 데이터의 크기가 약 33% 정도 늘어납니다.
일반 ASCII 문자로 인코딩하면 다음과 같은 시스템 비독립성의 문제들이 발생할 수 있습니다.
위와 같은 문제로 일반 ASCII 문자는 다양한 시스템 간의 데이터를 전달하기에 안전하지 않습니다.
Base64는 이러한 시스템 비독립성 문제를 해결하기 위해 ASCII 문자들 중 제어 문자와 일부 특수문자를 제외한 64개의 안전한 출력 문자만을 사용합니다.
(*안전한 출력 문자란, 문자 코드에 영향을 받지 않는 공통 ASCII를 의미)
즉, Base64는 HTML 또는 Email과 같이 문자를 위한 Media에 Binary Data를 포함해야 될 필요가 있을 때, 포함된 Binary Data가 시스템 독립적으로 동일하게 전송 또는 저장되는 걸 보장하기 위해 사용합니다.
Base64에서 62, 63번의 인코딩 문자값을 보면 각각 +
, /
입니다. 이는 연산자이기 때문에 데이터를 다룰 때 오류를 일으킬 수 있습니다. 또한, =
는 padding 문자에 해당하는데, 이는 URL에서 기존에 사용되고 있는 문자입니다. 따라서 URL Safe 한 base64의 62, 63번은 -
, _
로 대체되었고, =
는 생략하거나 특정 라이브러리는 .
로 대체합니다.
Spring, Interceptor
A.
Filter는 Spring Context의 외부, DispatcherServlet 앞에 존재합니다. 요청이나 응답 내용의 체크, 변경(인코딩 변환 등)등을 할 수 있습니다.
Interceptor는 Spring Context의 내부에 존재합니다.
DispatcherServlet이 Controller(Handler)를 호출하는 시점의 전, 후에 끼어들어 Controller(Handler)의 요청과 응답을 가로채서(Intercept) 처리합니다.
Spring Context의 내부에 존재하기 때문에, Spring의 모든 Bean 객체에 접근할 수 있습니다.
Interceptor는 여러 개를 사용할 수 있습니다. 로그인 체크, 권한 체크, 프로그램 실행시간 계산 작업, 로그 확인 등에 쓰입니다.
Interceptor methods
Custom Annotation + ArgumentResolver
로 분리할 수 있었다.Spring, Annotation, ArgumentResolver
OOP, Domain, Service
JDBC
암호화
A.
BCrypt는 동일한 원본 데이터에 대해 암호화를 할 때 마다 매번 다른 Hash 결과값이 나오는 암호 해시 함수입니다.
일반적인 Hash 함수는 원본 데이터가 동일하면, Hash 결과값도 동일합니다.
이럴 경우, Rainbow table 공격에 취약합니다.
Rainbow table은 원본 데이터와 Hash 결과값의 짝을 미리 저장해놓은 것을 말합니다.
이를 사용해 Hash 암호화 된 비밀번호로부터 원본 비밀번호를 추출합니다.
이를 Rainbow table 공격이라고 합니다.
BCrpyt 알고리즘은 동일한 원본 데이터라도 Hash 결과값이 매번 다르기 때문에, Rainbow table 공격을 방지할 수 있습니다.
BCrypt의 이러한 작동 방식이 가능한 이유는, Salt를 사용하기 때문입니다.
말 뜻 그대로 소금처럼 암호화를 할 때 추가적인 데이터를 첨가해, 매 번 결과값이 다르게 합니다.
BCrpyt 알고리즘은 Spring Security의 PasswordEncoder의 비밀번호 암호화 기본 전략입니다.
비밀번호 일치 검사를 할 때, 로그인 요청으로 들어온 비밀번호를 BCrpyt 알고리즘으로 암호화 합니다. 이 때, DB에 저장되어있는 암호화된 비밀번호를 Salt로 사용합니다. 이 결과가 DB에 저장되어있던 암호화된 비밀번호와 일치하면, 로그인 요청으로 들어온 비밀번호는 맞는 비밀번호임이 확인됩니다.
DB, Transaction
노선의 모든 구간들을 SELECT 한 뒤에, 구간 변경 요청의 유효성을 검사하고, DB를 업데이트 합니다.
여러 사용자가 이와 같은 요청을 동시에 하면 DB에 저장되는 지하철 구간이 꼬일 수 있습니다.
이를 해결하기 위해 DB 트랜잭션 격리수준을 어떻게 설정해야 할지 고민했습니다.
트랜잭션 격리수준은, 동시에 여러 트랜잭션들이 처리될 때, 각 트랜잭션끼리 얼마나 서로 격리되어 있는지를 나타내는 것입니다.
구체적으로 말해서, 특정 트랜잭션이 다른 트랜잭션이 변경한 데이터를 볼 수 있도록 허용할지 말지를 결정하는 것입니다.
다른 트랜잭션은 아직 Commit되지 않은 변경내용도 조회할 수 있다.
이런식으로 데이터 정합성에 문제가 많으므로, RDBMS 표준에서는 격리수준으로 인정하지도 않는다.
다른 트랜잭션은 Commit된 변경내용만 조회할 수 있다.
위의 경우를 다시 해보면,
B 트랜잭션이 1번 크루의 나이를 조회한 결과는 10살이다.
A 트랜잭션에서 Commit을 한 후에 B 트랜잭션에서 1번 크루의 나이를 조회해야 100살로 조회된다.
이 경우 NON-REPEATABLE READ
부정합 문제가 발생할 수 있다.
이는 하나의 트랜잭션 내에서 SELECT를 여러 번 했을 경우, 항상 같은 결과가 조회되어야 한다는 REPEATABLE READ
정합성에 어긋난다.
트랜잭션이 시작되기 전에 Commit된 내용만 조회할 수 있다.
위에서 언급한 NON-REPEATABLE READ
부정합 문제가 발생하지 않는다.
이 격리 수준 이하에서는 Phantom Read 문제가 발생한다.
가장 단순하고 가장 엄격한 격리수준이다.
격리수준이 SERIALIZABLE일 경우 읽기 작업에도 공유 잠금을 설정하게 되고, 이렇게 되면 동시에 다른 트랜잭션에서 이 레코드를 변경하지 못하게 된다.
이러한 특성 때문에 동시처리 능력이 다른 격리수준보다 떨어지고, 성능저하가 발생한다.
Fare
값 객체로 포장했다.OOP
github에서 최신 코드 가져오기 -> 빌드 -> 기존에 실행중인 애플리케이션 종료 -> 새로운 버전의 애플리케이션 실행
의 과정을 일일이 명령어를 쳐서 실행해야 했다.배포
CORS
정책 위반 에러가 발생했다.Access-Control-Allow-Origin
헤더 설정을 통해 해당 에러를 해결했다.Web, CORS
A.
브라우저의 보안 정책입니다.
브라우저에 랜더링 된 특정 페이지는 자신의 출처의 리소스만 불러올 수 있습니다.
다른 출처의 리소스를 불러오려면 해당 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야 합니다.
브라우저는 서버에 본 요청을 일단 즉시 보냅니다.
응답 헤더에 CORS 허가 내용이 있다면 정상으로 받아들이고, 그렇지 않다면 브라우저에서 CORS 에러를 발생시킵니다.
이 방식은 본 요청이 무조건 서버에 가기 때문에, 보안상 취약합니다.
따라서 이 방식은 특정 조건을 만족하는 요청만 보낼 수 있습니다.
Simple Request에 해당하지 않는 요청은 브라우저가 Preflighted Request 요청을 본 요청보다 먼저 서버에 보내봅니다. OPTIONS 메소드 요청을 통해 이런 요청을 할 건데, CORS허가가 되는지 서버에 미리 물어봅니다. 서버가 허가된다고 응답하면, 그 때 본 요청을 보냅니다.
허가된 CORS 요청만 보낼 수 있으므로, 보안상 더욱 유리합니다.
Docs, REST
A.
프로덕션 코드와 문서의 동기화를 가장 중요하게 생각해 Spring Rest Docs를 적용했습니다.
Swagger는 문서와 프로덕션 코드가 동기화 되지 않을 수 있다는 단점이 있습니다.
반면에 Spring Rest Docs는 테스트가 성공해야 문서화가 되기 때문에 프로덕션 코드와 문서가 항상 동기화됩니다.
이러한 이유로 저는 Spring Rest Docs를 선택했습니다.
OOP, Java
A.
요금 정책 각각을 Enum 상수로 정의했습니다.
각 Enum 상수에, 해당 정책의 조건을 나타내는 람다식과 정책 객체를 반환하는 함수를 함수형 인터페이스 타입의 인스턴스 변수로 지정했습니다.
이를 통해 정책마다 추가되는 if문을 제거할 수 있었습니다.
람다식이란 메서드를 하나의 식으로 표현한 것입니다.
람다식은 사실 메서드가 하나만 존재하는 익명 클래스의 객체입니다.
메서드가 하나만 존재하는 익명 클래스의 객체를 최대한 간소화시켜 식의 형태로 표현한 것이 람다식 입니다.
람다식을 사용하려면 이를 참조할 타입이 있어야 합니다.
람다식은 메서드가 하나만 존재하는 익명 클래스의 객체이기 때문에, 오직 하나의 추상메서드만 정의되어 있는 인터페이스 타입으로 참조하기로 했고, 이를 함수형 인터페이스라고 합니다.
static 메서드와 default 메서드의 개수에는 제약이 없습니다.