2023. 9. 5. 22:53ㆍDevelopment
안녕하세요!
지금부터 본격적인 구현에 들어가 보려고 해요.
저번 시간에 정리한 설계 순서대로 구현을 할 예정이니, 이전 게시물을 보고 오시는 것을 추천드려요.
https://dalmeng-commeng.tistory.com/4
JWT 기반 인증 2편 - 스프링 시큐리티 JWT 설계
안녕하세요! 이번 시간에는 스프링 시큐리티에서 JWT의 인증 흐름을 다뤄보려고 해요. 스프링 시큐리티에서 JWT를 구현하기 위한 설계 단계라고 봐주셨으면 좋겠어요. 소프트웨어 공학에서는 설
dalmeng-commeng.tistory.com
이 게시물은 JWT의 구현에 집중하고 있어서 구현에 사용되는 라이브러리, 프레임워크 등 기본적인 것은 설명하고 있지 않아요.
아래 내용들은 설명하고 넘어가는 내용이 아니니 모르는 것이 있으시면 보고 오시는 것이 좋아요.
- Basic Java Language: Interface, Inheritance, Generic, ...
- Basic Concepts of Spring : Bean, Dependency Injection, ...
- Lombok
- Spring Data JPA
- Spring Security
- Hierarchy Structure of Spring: Controller, Service, Repository
제가 개발하고 있는 소프트웨어 환경은 이렇게 돼요.
- OS : MacOS 13.3
- Programming Language : Java 17
- Framework : Spring Boot 3.1.3
- Build Tool : Gradle 8.2.1
- DBMS : MySQL 8.1.0
저번 시간에 설명한 대로 순서대로 진행해볼게요.
1. JWT란? - What is JWT?
2. 스프링 시큐리티 JWT 설계
3. 스프링 시큐리티에서 JWT 인증 · 인가 구현 - Implementation of JWT Based Authentication · Authorization in Spring Security
3 - 1. 라이브러리 사용을 위한 의존성 추가 - Library Dependency
3 - 2. 데이터베이스와 WAS 연동 - Connection Between Database and WAS
3 - 3. 일관적인 API 응답을 위한 Base Response 구현 - Base Response for Consistent API Response
3 - 4. 스프링 시큐리티에서 인증 시 사용하는 UserDetails 객체와 UserDetailsService 객체 구현
3 - 5. JWT Provider 구현
3 - 6. JWT 필터 구현
3 - 7. 서비스 계층 구현
3 - 8. 예외 핸들러 구현
3 - 9. 컨트롤러 계층 구현
3 - 10. 스프링 시큐리티 설정
3 - 11. 테스트
4. Refresh Token 도입
5. 리팩토링
구현부터는 양이 좀 많아져서 여러 게시물로 나눠서 올릴 예정이에요.
3 - 1. 라이브러리 사용을 위한 의존성 추가 - Library Dependency
https://start.spring.io/에서 스프링 프로젝트를 쉽게 만들 수 있어요.
스프링부트 버전, 자바 버전, 빌드 도구, 의존성 등 여러 설정들을 할 수 있어요.

여기서 의존성을 추가하지 않아도 설정 파일에서 추가할 수도 있어요.
1. 스프링 웹 어플리케이션 / 테스트 의존성
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
2. 스프링 시큐리티 관련 의존성
implementation 'org.springframework.boot:spring-boot-starter-security'
3. JWT 라이브러리 의존성
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
4. 데이터베이스 관련 의존성
implementation 'mysql:mysql-connector-java:8.0.32'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
5. Lombok 의존성
annotationProcessor 'org.projectlombok:lombok'
compileOnly 'org.projectlombok:lombok'
위 의존성들을 build.gradle의 dependencies에 넣어줄게요.
의존성 코드를 넣는 것만으로도 스프링부트가 의존성 관리를 해줘서 별도의 설치 없이 바로 사용할 수 있어요.
3 - 2. 데이터베이스와 WAS 연동 - Connection Between Database and WAS
1단계 : 설정 파일 작성하기
데이터베이스와 연동시키기 위해 데이터베이스에 대한 정보를 넣어줘야 해요.
src/main/resources/application.properties에 설정을 넣어주도록 할게요.
1. 데이터베이스 드라이버
여기에는 사용할 데이터베이스를 명시해줘요.
저는 MySQL을 사용했어요.
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
2. 데이터베이스 서버 정보
데이터베이스 서버의 IP와 포트, 그리고 데이터베이스를 명시해줘요.
형식은 아래와 같아요.
jdbc:mysql://{IP Address}:{Port}/{Database Name}
저는 이렇게 작성했어요.
spring.datasource.url=jdbc:mysql://localhost:3306/testdb
데이터베이스 서버에 접속하기 위해 사용자 이름과 비밀번호를 적어줘요.
spring.datasource.username=dalmeng
spring.datasource.password=12345678
3. [선택 사항] DDL 옵션
JPA의 DDL 정책을 설정해줄 수 있어요.
DDL은 Data Definition Language로, 테이블을 만들거나 수정하는 쿼리, 즉 Create, Drop 등을 말해요.
JPA는 @Entity 어노테이션이 있는 클래스를 찾아 DDL을 이용해 자동으로 테이블을 만들고 수정해줘요.
JPA의 DDL 옵션 종류를 간단하게 설명해드릴게요.
- Create : 기존 테이블을 삭제하고 다시 만들어요. 그렇기 때문에 기존에 존재하던 데이터가 모두 날라가요. 이 옵션은 매번 새로운 환경을 만들기 때문에 개발 중에만 사용해야해요. 개발 중에도 데이터가 날라가면 안 되는 환경에서는 사용하면 안 돼요.
- Update: 변경된 부분만 적용해요. 이 옵션도 실제 운영 환경에서는 사용하지 않는 것이 좋아요.
- Validate: 엔티티와 테이블이 정상적으로 매핑되었는지만 확인해요. 보통 이 옵션을 실제 환경에서 사용하기도 해요.
저는 Validate 옵션을 사용했어요.
spring.jpa.hibernate.ddl-auto=validate
4. [선택 사항] SQL 쿼리 보기
SQL 쿼리가 날라갈 때마다 터미널 창에서 보여줘요,
우리는 Spring Data JPA의 Query Method나 JPQL로 쿼리를 날릴 텐데, 하이버네이트가 실제로 DBMS에 날리는 쿼리를 보여줘요.
spring.jpa.properties.hibernate.show_sql=true
2단계 : 엔티티 작성하기
엔티티(Entity)는 우리의 데이터베이스 내 실제 테이블과 매핑돼요.
우리는 인증에 필요한 사용자 데이터만 있으면 돼서, 사용자 테이블 하나만 정의하면 돼요.

제가 설계한 테이블 구조예요.
간단한 테이블이기 때문에 다들 이해할 수 있을 거예요.
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String role;
}
설계한대로 Entity 클래스를 작성했어요.
이렇게 엔티티 클래스만 작성해줘도 JPA에서 테이블을 만들어줘요.
우리가 직접 테이블을 만드는 쿼리를 날릴 필요가 없죠.
3단계 : Repository 작성하기
우리는 Spring Data JPA로 쉽게 데이터베이스를 사용할 거예요.
Spring Data JPA는 Repository 기반으로 Query Method를 사용해요.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
기본적인 CRUD는 추가적인 정의 없이 바로 사용할 수 있고, 사용자 이름으로 사용자를 찾는 메소드만 추가적으로 정의해줬어요.
(참고: CRUD란 Create, Read, Update, Delete를 뜻해요. 각각 insert, select, update, delete에 해당되고, 이는 DML(Date Manipulation Language)에 속해요.)
3 - 3. 일관적인 API 응답을 위한 Base Response 구현 - Base Response for Consistent Response Format
프로그래밍의 구현에서 중요한 요소 중 하나가 표준이에요.
이 경우에는 프론트엔드 측과 백엔드 측 소통 간 표준이에요.
프론트엔드 측에서는 요청이 성공했는지, 실패했는지, 실패했으면 왜 실패했는지 쉽게 알 수 있어야해요.
그래서 백엔드 측에서는 항상 같은 형식으로 반환해서 프론트 측과 소통이 잘 되도록 설계하는 것이 좋아요.
이 프로젝트에서는 Base Response를 사용해서 응답 표준을 만들었어요.
아래는 제가 작성한 Base Response의 구조예요.

success 변수는 요청이 성공했는지, 실패했는지 알려주는 변수예요.
서버에서 예외가 발생하거나 프론트 측에서 예상한 응답과 다른 경우에 false가 들어가고, 성공적으로 요청을 처리했으면 true가 들어가요.
resultCode는 요청 결과를 알려주는 변수예요.
이 또한 프론트 측과 미리 협의가 되어있어야 해요.
저 같은 경우는 아래와 같이 표준을 세웠어요.
200 : 요청이 성공한 경우
400 : 백엔드 측에서 제어할 수 없거나 예측하지 못한 오류, 예외가 일어난 경우
404 : 해당 이름을 가진 사용자가 없을 경우
405 : 해당 자원에 접근할 수 없는 사용자가 요청한 경우
msg는 요청 결과를 부가적으로 설명해주는 변수예요.
200, 404, 405는 우리가 미리 협의해서 결과를 알고 있지만, 400같은 경우는 예측하지 못한 곳에서 오류, 예외가 발생했기 때문에 이 변수를 통해 알려주어야 겠다고 생각했어요.
data는 응답 데이터예요.
요청마다 데이터 타입을 정해줄 필요 없이 제네릭으로 처리해서 어떤 형식의 데이터라도 편하게 넣을 수 있도록 해주었어요.


위와 같은 형식으로 응답이 돌아와요.
다른 결과지만 골격이 똑같아서 프론트에서 처리하기 쉽겠죠?
OK와 FAILED 메소드는 성공과 실패를 쉽게 구분하기 위해서 만들었어요.
만약 요청을 성공했을 때,
return new BaseResponse(true, 200, "Succeed", data);
라고 쓰는 것보다
return BaseResponse.OK(data);
라고 쓰는 것이 간단하고 훨씬 이해하기 편하죠?
다른 예시로 요청이 실패했을 때,
return new BaseResponse(false, 404, "User Not Found", null);
라고 쓰는 것보다
return BaseResponse.FAILED(404, "User Not Found");
라고 쓰는 것이 훨씬 직관적이죠?
그래서 OK와 FAILED로 응답을 더 쉽고 빠르고, 직관적으로 할 수 있도록 했어요.
@Getter
@RequiredArgsConstructor
public class BaseResponse<T> {
private final Boolean success;
private final int resultCode;
private final String msg;
private final T data;
public static BaseResponse OK(){
return new BaseResponse(true, 200, "Succeed", null);
}
public static <T> BaseResponse OK(T data){
return new BaseResponse(true, 200, "Succeed", data);
}
public static BaseResponse FAILED(){
return new BaseResponse(false, 400, "Failed", null);
}
public static <T> BaseResponse FAILED(T data){
return new BaseResponse(false, 400, "Failed", data);
}
public static <T> BaseResponse FAILED(int resultCode, T data){
return new BaseResponse(false, resultCode, "Failed", data);
}
public static <T> BaseResponse FAILED(int resultCode, String msg, T data){
return new BaseResponse(false, resultCode, msg, data);
}
public static <T> BaseResponse FAILED(String msg, T data){
return new BaseResponse(false, 400, msg, data);
}
}
제네릭(Generic)을 사용해서 데이터 형식에 구애받지 않고 응답을 할 수 있게 했어요.
어떨 때는 응답 데이터가 없을 수도 있고, 응답 코드를 적지 않아도 될 때도 있고 상황이 다양하죠?
그래서 오버로딩(Overloading)을 이용해서 같은 이름으로 다양하게 사용할 수 있게 했어요.
(참고 : 오버라이딩(Overriding)은 부모 클래스의 메소드를 재정의하는 것이고, 오버로딩(Overloading)은 같은 이름이지만 인자 등을 다르게 해서 다양한 목적으로 사용할 수 있게 하는 것을 말해요.)
이야기가 길어져서 나머지 내용은 다음 게시물로 넘길게요.
다음 게시물부터 본격적으로 스프링 시큐리티와 JWT 라이브러리를 건드려 볼 예정이에요.
제 글이 많은 도움이 되었길 바라며,
긴 글 봐주셔서 감사합니다!
멋진 개발자가 되기 위해 더 열심히 달리겠습니다!
- 달맹 -
'Development' 카테고리의 다른 글
| JWT 기반 인증 5편 - 스프링 시큐리티 JWT 인증 · 인가 구현 (3) (0) | 2023.09.08 |
|---|---|
| JWT 기반 인증 4편 - 스프링 시큐리티 JWT 인증 · 인가 구현 (2) (1) | 2023.09.07 |
| JWT 기반 인증 2편 - 스프링 시큐리티 JWT 설계 (0) | 2023.09.05 |
| JWT 기반 인증 1편 - JWT란? (0) | 2023.09.04 |
| JPA와 Hibernate, 그리고 JDBC (0) | 2023.09.01 |