프로젝트 문제
1. 로그인이 성공한 후 토큰이 어디서 만들어지고 저장되는지
클라이언트가 로그인 요청을 하면, 서버는 데이터베이스에서 사용자 정보를 확인한 후 인증 절차를 거칩니다. JWT 토큰은 JwtAuthenticationFilter의 successfulAuthentication 메서드에서 생성되는데요, 이 과정에서 사용자 정보를 기반으로 토큰이 만들어집니다.
이때 생성된 토큰은 createToken 메서드를 통해 서명되며, 이를 HTTP 응답의 쿠키에 저장해 클라이언트로 전달하게 됩니다. addJwtToCookie 메서드를 통해 이 과정이 처리되죠. 이후 클라이언트는 이 쿠키를 통해 서버에 인증된 요청을 보낼 수 있습니다. 그래서 클라이언트는 토큰을 로컬 스토리지나 세션 스토리지가 아니라, 쿠키에 저장합니다.
2. 토큰에 들어있는 정보는 무엇인가
JWT 토큰에는 헤더, 페이로드, 서명으로 구성된 정보가 들어갑니다. 헤더에는 주로 알고리즘 정보가 포함되고, 페이로드에는 사용자 정보와 토큰 만료 시간 등이 들어가죠. 예를 들면, 페이로드에 sub 필드는 사용자 ID를 나타내고, exp 필드는 토큰의 만료 시간을 의미합니다.
마지막으로 서명 부분은 토큰의 무결성을 검증하기 위한 알고리즘으로 생성되며, 이렇게 생성된 서명으로 토큰의 변조 여부를 확인할 수 있습니다.
3. JWT 토큰이 서버에서 생성되고 클라이언트에 전달되는 과정
JWT 토큰이 생성되는 과정은 다음과 같습니다.
- 클라이언트가 로그인 요청을 보냅니다. 이때 사용자 이름과 비밀번호를 포함한 자격 증명이 함께 전달됩니다.
- 서버는 JwtAuthenticationFilter에서 이 요청을 받아 인증을 시도합니다. attemptAuthentication 메서드를 통해 사용자의 자격 증명을 확인하죠.
- 사용자가 인증되면 successfulAuthentication 메서드에서 JWT 토큰이 생성됩니다. 토큰에는 사용자 정보와 유효 기간이 포함됩니다.
- 생성된 토큰은 HTTP 응답의 쿠키에 저장되어 클라이언트로 전송됩니다. 클라이언트는 이후 쿠키에 저장된 토큰을 통해 인증된 요청을 자동으로 서버에 보내게 됩니다.
4. 토큰은 몇 개를 생성하는가
현재 저희 시스템에서는 하나의 JWT 토큰만 생성하고 있습니다. 즉, 액세스 토큰만을 사용하고, 리프레시 토큰은 사용하지 않는 방식입니다. 이는 소규모 애플리케이션이나 보안 요구 사항이 상대적으로 낮은 경우에 적합하며, 토큰이 만료되면 사용자가 다시 로그인하도록 요구하는 방식입니다.
5. 토큰을 어떻게 사용자에게 전달하는가
사용자가 로그인을 성공하면, 서버는 JWT 토큰을 생성합니다. 이 토큰은 HttpServletResponse 객체를 사용해 HTTP 응답의 쿠키에 저장됩니다. 브라우저는 이 쿠키를 자동으로 관리하며, 사용자가 요청을 보낼 때마다 쿠키에 저장된 토큰을 서버로 전송하게 됩니다.
따라서 로컬 스토리지나 세션 스토리지에 별도로 토큰을 저장하지 않아도, 브라우저는 자동으로 쿠키를 통해 인증을 처리할 수 있습니다.
6. 에러 코드 및 예외 처리
저는 애플리케이션의 전역 예외 처리를 위해 GlobalExceptionHandler를 사용합니다. 이 클래스는 @RestControllerAdvice 어노테이션을 통해 애플리케이션 전반에 걸쳐 발생하는 예외를 중앙에서 관리할 수 있게 해줍니다.
- 상황에 맞는 커스텀 예외를 정의했습니다. 예를 들어, TodoNotFoundException, UserNotFoundException과 같은 예외를 정의하여 404 상태 코드와 함께 적절한 메시지를 반환합니다.
- 에러 코드 관리를 위해 저는 TodoErrorCode 클래스를 사용합니다. 이 클래스는 todo, comment, user와 관련된 예외 상황에 대해 일관된 에러 코드와 메시지를 제공합니다. 이를 통해 클라이언트가 각 상황에 맞는 에러 코드를 쉽게 처리할 수 있습니다.
- 공통적인 예외 처리로는 401(Unauthorized), 403(Forbidden*과 같은 상태 코드를 사용하여 클라이언트에 일관된 에러 메시지를 반환합니다. 예를 들어, 유효하지 않은 토큰이 있을 경우 401 상태 코드와 함께 '토큰이 유효하지 않습니다'라는 메시지를 반환하고, 권한이 없는 사용자가 수정/삭제를 시도할 경우 403 상태 코드와 함께 '작성자만 삭제/수정할 수 있습니다'라는 메시지를 반환합니다.
그 외에도 상황별로 다양한 예외 처리를 적용했습니다.
- 토큰 오류: 사용자가 유효하지 않은 토큰을 사용할 경우 401 상태 코드와 '토큰이 유효하지 않습니다' 메시지를 반환합니다.
- 작성자 확인 오류: 작성자가 아닌 사용자가 할일을 수정 또는 삭제하려 할 때는 403 상태 코드와 함께 '작성자만 삭제/수정할 수 있습니다' 메시지를 반환합니다.
- 중복 사용자: 이미 등록된 사용자 이름으로 회원가입을 시도할 경우 400 상태 코드와 '중복된 username 입니다' 메시지를 반환합니다.
- 로그인 실패: 잘못된 사용자 이름이나 비밀번호로 로그인하려 할 경우 404 상태 코드와 '회원을 찾을 수 없습니다' 메시지를 반환합니다.
기술면접 문제
7. 트랜잭션이란 무엇인가?
트랜잭션은 데이터베이스에서 하나의 작업을 완전히 처리하기 위한 일련의 작업 단위를 의미합니다. 트랜잭션은 데이터의 정합성을 보장하기 위해 여러 작업을 하나의 논리적 단위로 묶어, 그 작업들이 모두 성공하거나, 실패 시 모두 롤백되도록 하는 역할을 합니다.
트랜잭션은 데이터베이스에서 삽입, 삭제, 수정 같은 작업이 연속적으로 일어날 때, 데이터의 일관성과 무결성을 유지하기 위한 중요한 개념입니다. 만약 트랜잭션 내의 어떤 작업이 실패하면, 모든 변경사항이 이전 상태로 되돌아가도록 보장됩니다.
예를 들어, 은행 계좌 이체의 경우, A 계좌에서 돈을 빼는 작업과 B 계좌로 돈을 입금하는 작업이 모두 성공해야 완전히 처리된 것으로 간주됩니다. 만약 중간에 네트워크 오류가 발생하거나 시스템 장애가 발생한다면, A 계좌에서 돈만 빠져나가고 B 계좌에 입금이 안 되는 상황을 방지하기 위해 트랜잭션이 실패 시 롤백됩니다.
결론적으로 트랜잭션은 데이터베이스의 일관성과 무결성을 유지하기 위한 중요한 작업 단위입니다. 하나의 트랜잭션 내의 작업이 모두 성공해야 하고, 그렇지 않으면 전부 롤백되어야 하는 것이 트랜잭션의 핵심입니다
8. 트랜잭션의 ACID 특징에 대해 설명해주세요.
트랜잭션의 ACID 특성은 Atomicity(원자성), Consistency(일관성), Isolation(고립성), **Durability(지속성)**을 의미하며, 이는 데이터베이스의 트랜잭션이 안정적으로 처리될 수 있도록 보장하는 핵심입니다.
ACID 특성을 하나씩 설명하겠습니다.
- Atomicity(원자성): 트랜잭션 내 모든 작업은 하나의 단위로 실행되며, 그 중 하나라도 실패하면 전체 작업이 롤백됩니다. 즉, 작업이 모두 성공하거나 모두 실패해야 합니다.
- 예를 들어, 계좌 이체에서 돈을 출금하고 입금하는 두 작업이 모두 성공해야만 이체가 완료됩니다. 한 작업이라도 실패하면 트랜잭션 전체가 롤백됩니다.
- Consistency(일관성): 트랜잭션이 성공적으로 완료되면, 데이터베이스는 일관된 상태를 유지해야 합니다. 트랜잭션 전후에 데이터베이스가 정합성 규칙을 준수해야 하는 것이죠.
- 예를 들어, 계좌 이체 후에 각 계좌의 총합이 이전과 동일해야 한다는 데이터 일관성을 보장합니다.
- Isolation(고립성): 여러 트랜잭션이 동시에 실행될 때, 각 트랜잭션은 서로 간섭하지 않아야 하며, 독립적으로 처리되어야 합니다. 다른 트랜잭션의 중간 상태를 볼 수 없다는 의미입니다.
- 즉, 하나의 트랜잭션이 완료되기 전까지 다른 트랜잭션은 그 영향을 받아서는 안 됩니다.
- Durability(지속성): 트랜잭션이 성공적으로 완료되면, 그 결과는 영구적으로 데이터베이스에 저장되어야 합니다. 시스템 장애가 발생해도 트랜잭션의 결과는 유지됩니다.
- 예를 들어, 전원이 갑자기 꺼지더라도 트랜잭션이 성공적으로 완료된 경우, 데이터는 손실되지 않습니다.
ACID 특성은 트랜잭션이 안전하고 신뢰성 있게 처리되도록 보장하는 핵심 원칙입니다. 이를 통해 데이터베이스의 무결성과 일관성을 유지하고, 동시에 데이터 손실이나 불일치를 방지할 수 있습니다. 각 특성은 데이터베이스가 신뢰할 수 있는 트랜잭션 관리를 수행할 수 있도록 하는 중요한 요소입니다.
9. PostgreSQL을 선택한 이유는 무엇일까요?
저는 PostgreSQL을 선택한 이유로 오픈소스의 유연성, 표준 준수 및 고급 기능 제공을 꼽을 수 있습니다. 특히 PostgreSQL은 데이터 무결성을 보장하는 강력한 트랜잭션 관리와 확장성 있는 데이터 처리 능력을 가지고 있어, 복잡한 쿼리나 대규모 데이터베이스 운영에도 적합하다고 판단했습니다."
PostgreSQL을 선택한 구체적인 이유를 몇 가지로 설명드리겠습니다.
- 1) ACID 특성을 완벽하게 지원: PostgreSQL은 ACID(Atomicity, Consistency, Isolation, Durability) 트랜잭션 특성을 엄격히 준수합니다. 데이터 무결성과 일관성이 중요한 프로젝트에서는 트랜잭션을 안정적으로 처리할 수 있는 PostgreSQL이 매우 유리합니다. 이는 복잡한 비즈니스 로직을 구현할 때 필수적인 요소입니다.
- 2) 오픈소스와 비용 절감: PostgreSQL은 완전한 오픈소스로, 추가 라이선스 비용이 들지 않으며, 상업적 환경에서도 무료로 사용할 수 있습니다. 반면, Oracle이나 일부 상용 DBMS는 비용이 많이 들기 때문에, PostgreSQL이 비용 대비 효율적인 선택이 됩니다.
- 3) 표준 준수와 호환성: PostgreSQL은 SQL 표준을 준수하는 데이터베이스로, 복잡한 쿼리 처리나 트리거, 함수 등의 고급 기능을 많이 제공합니다. 또한 JSON, XML 같은 다양한 데이터 타입을 지원하기 때문에 NoSQL 기능을 겸비하여 유연한 데이터 모델링이 가능합니다. 이로 인해 MySQL이나 Oracle에 비해 더 강력한 쿼리 처리 능력을 제공합니다.
- 4) 확장성과 확장성 있는 데이터 타입 지원: PostgreSQL은 수평 확장성이 뛰어나고, 다양한 플러그인을 통해 기능 확장이 가능합니다. 특히 PostGIS 같은 확장 기능을 통해 지리 정보 시스템을 구축하거나, 여러 데이터 타입을 자유롭게 사용할 수 있어, 다양한 프로젝트에 적합한 선택지입니다.
- 5) 성능 및 확장성: PostgreSQL은 대규모 데이터를 효율적으로 처리할 수 있도록 설계되어 있어, 대용량 트랜잭션 처리나 동시 사용자 처리에 강점을 보입니다. 또한 다중 버전 동시성 제어(MVCC)를 통해 여러 사용자가 동시에 데이터를 수정할 때도 성능 저하 없이 안정적인 처리가 가능합니다.
요약하면, PostgreSQL은 데이터 무결성을 보장하는 트랜잭션 처리, 오픈소스의 비용 절감 효과, 그리고 확장성 있는 고급 기능 지원 덕분에 다양한 요구사항을 충족시킬 수 있는 매우 강력한 데이터베이스입니다. 프로젝트의 확장성과 복잡한 비즈니스 로직이 필요한 상황에서 PostgreSQL은 탁월한 선택이라고 생각합니다.
10. Spring Boot의 DispatcherServlet에 대해서 설명해주세요.
DispatcherServlet은 Spring MVC 프레임워크의 핵심 구성 요소로, 모든 HTTP 요청을 중앙에서 처리하는 프론트 컨트롤러입니다. 클라이언트로부터 들어온 요청을 받아서 적절한 컨트롤러로 전달하고, 처리된 결과를 다시 응답으로 반환하는 역할을 합니다.
DispatcherServlet은 웹 애플리케이션의 프론트 컨트롤러로서, 다음과 같은 방식으로 동작합니다.
- 요청 수신: 클라이언트가 웹 애플리케이션에 요청을 보내면, 그 요청은 모두 DispatcherServlet이 받게 됩니다.
- 핸들러 매핑: HandlerMapping을 사용해 해당 요청을 처리할 적절한 컨트롤러를 찾습니다. 여기서 URL 경로, HTTP 메서드 등을 기준으로 매핑됩니다.
- 핸들러 실행: 적절한 컨트롤러 메서드를 실행하고, 그 결과를 ModelAndView 객체로 반환합니다.
- 뷰 리졸버: ViewResolver를 사용해 반환된 결과를 적절한 뷰로 매핑하여, JSP나 Thymeleaf 같은 뷰 템플릿을 결정합니다.
- 응답 전송: 마지막으로 DispatcherServlet은 해당 뷰를 사용해 클라이언트에게 최종 결과를 응답합니다.
이러한 과정을 통해 DispatcherServlet은 웹 요청을 중앙에서 일괄적으로 관리하고, 애플리케이션의 흐름을 조율하는 역할을 합니다.
요약하면, DispatcherServlet은 Spring MVC의 핵심 컨트롤러로서, 모든 요청을 처리하고 적절한 응답을 반환하는 역할을 수행합니다. 이를 통해 애플리케이션의 요청 처리 흐름이 일관되게 유지될 수 있습니다.
11. Bean과 IoC에 대해서 설명해주세요.
Bean은 Spring 컨테이너에서 관리되는 객체를 의미하며, IoC는 객체의 생성과 생명주기 관리를 애플리케이션 코드가 아닌 Spring 컨테이너가 담당하는 개념입니다. 이를 통해 객체 간의 의존성 관리가 훨씬 효율적으로 이루어집니다.
2. 과정:
Spring에서 Bean은 Spring 컨테이너에서 생성되고 관리되는 객체로, 주로 @Component, @Service, @Repository 같은 어노테이션을 통해 정의됩니다. Bean은 애플리케이션에서 필요한 객체들을 싱글톤으로 관리하고, 필요할 때 주입하여 사용됩니다.
IoC는 객체 생성과 관련된 제어권을 개발자가 아닌 Spring 컨테이너에 위임하는 개념입니다. 이를 통해 객체 간의 결합도를 낮추고, 유연한 의존성 관리가 가능해집니다.
Spring에서는 의존성 주입(DI)을 통해 Bean 객체를 생성하고 필요한 곳에 자동으로 주입합니다. 개발자는 객체를 명시적으로 생성하지 않고, Spring 컨테이너가 제공하는 Bean을 사용해 유연하고 재사용성 있는 코드를 작성할 수 있습니다.
결론적으로, Bean은 Spring 컨테이너에서 관리되는 객체이고, IoC는 객체의 생성과 관리를 Spring 컨테이너가 담당하는 개념입니다. 이로 인해 애플리케이션은 유연성과 확장성을 가지며, 의존성 관리가 쉬워집니다.
12. Filter, Interceptor, AOP에 대해서 설명해주세요.
Filter, Interceptor, AOP는 모두 요청 처리의 흐름을 제어하거나 공통 기능을 분리하는 데 사용되지만, 그 목적과 사용되는 위치에 차이가 있습니다. 각각의 기술은 요청 전/후의 전처리와 후처리, 비즈니스 로직과의 분리를 효과적으로 도와줍니다.
각각의 차이점을 설명드리겠습니다.
- Filter: Filter는 서블릿 컨테이너에서 제공하는 기능으로, HTTP 요청과 응답의 흐름을 처리합니다. 주로 서블릿 요청 전에 실행되어 인증, 인코딩 같은 전역적인 처리에 사용됩니다. **필터 체인(Filter Chain)**을 통해 여러 필터가 순차적으로 실행될 수 있습니다.
- 예시로, CORS 설정, 로깅, 보안 관련 작업은 주로 필터에서 처리됩니다.
- Interceptor: Interceptor는 Spring의 HandlerInterceptor 인터페이스를 구현하여, 컨트롤러의 요청 처리 전, 후에 특정 작업을 수행할 수 있습니다. Filter와 달리, Spring MVC 컨텍스트에서 동작하며, 요청을 가로채서 추가 로직을 삽입할 수 있습니다.
- 주로 로그인 체크, 권한 검증 등 비즈니스 로직과 관련된 작업이 Interceptor에서 처리됩니다.
- AOP(Aspect-Oriented Programming): AOP는 비즈니스 로직과 공통 로직을 분리하기 위한 방법으로, **애플리케이션의 횡단 관심사(Cross-Cutting Concerns)**를 처리합니다. 이를 통해 중복된 코드를 제거하고, 로그 작성, 트랜잭션 관리, 보안 처리와 같은 공통 로직을 AOP로 관리할 수 있습니다.
- AOP는 메서드 실행 전후, 예외 발생 시 등에 공통 기능을 주입할 수 있으며, 이를 통해 코드의 응집도를 높이고, 중복을 줄일 수 있는 장점이 있습니다.
즉, Filter는 HTTP 요청/응답의 전처리를, Interceptor는 Spring MVC 컨트롤러 전후의 작업을 처리하며, AOP는 비즈니스 로직과 공통 로직을 분리하여 코드의 유연성과 재사용성을 높이는 데 사용됩니다. 각 기술은 처리되는 위치와 목적에 따라 적절하게 사용됩니다.