
25일차에 학습한 Spring Boot + MyBatis 기본 계층 구조를 바탕으로 회원가입과 로그인 기능을 구현하고 CORS, JWT, HTTP Header, 환경변수(.env), Profile(dev/prod)까지 적용하며 실제 서비스에 가까운 백엔드 구조로 확장했다.
Optional을 이용한 로그인 결과 처리ResponseEntity를 이용한 HTTP 응답 제어.env를 이용한 DB 설정값 외부화application.yml, application-dev.yml, application-prod.yml 분리testcase 프로젝트와 26일차 실제 서비스 구조의 차이이번 프로젝트의 주요 파일 구조는 다음과 같다.
C:\inpire_6th\BE\inspire_mybatis
│
├─ .env
│
└─ src
└─ main
├─ java
│ └─ com.example.inspire_mybatis
│
│ ├─ features
│ │ ├─ commons
│ │ │ ├─ config
│ │ │ │ └─ CorsConfig.java
│ │ │ │
│ │ │ └─ token
│ │ │ └─ JwtProvider.java
│ │ │
│ │ └─ users
│ │ ├─ ctrl
│ │ │ └─ UserController.java
│ │ │
│ │ ├─ domain
│ │ │ └─ dto
│ │ │ ├─ UserRequestDTO.java
│ │ │ └─ UserResponseDTO.java
│ │ │
│ │ ├─ repository
│ │ │ └─ UserMapper.java
│ │ │
│ │ └─ service
│ │ └─ UserService.java
│ │
│ └─ InspireMybatisApplication.java
│
└─ resources
├─ mappers
│ └─ userMapper.xml
│
├─ application.yml
├─ application-dev.yml
└─ application-prod.yml
전체적인 요청 처리 흐름은 다음과 같다.
React
localhost:3000
↓
CorsConfig
↓
UserController
↓
UserService
↓
UserMapper
↓
userMapper.xml
↓
MariaDB
로그인에서는 JWT 관련 기능이 추가된다.
UserController
↓
UserService
├──────────────→ UserMapper
│ ↓
│ userMapper.xml
│ ↓
│ DB
│
└──────────────→ JwtProvider
↓
Access Token
Refresh Token
CORS는 Cross-Origin Resource Sharing의 약자로 서로 다른 Origin 간 HTTP 요청을 허용하기 위한 정책이다.
현재 개발 환경은 다음과 같이 구성되어 있다.
React
http://localhost:3000
Spring Boot
http://localhost:8000
Port가 다르기 때문에 브라우저에서는 서로 다른 Origin으로 판단한다.
이를 허용하기 위해 CorsConfig를 작성했다.
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("*")
.allowedOriginPatterns("http://localhost:3000")
.allowedMethods(
"GET",
"POST",
"DELETE",
"PUT",
"PATCH",
"OPTIONS"
);
}
}
registry.addMapping("/**")
모든 URL에 CORS 설정을 적용한다.
.allowedOriginPatterns("http://localhost:3000")
React 개발 서버에서 들어오는 요청을 허용한다.
.allowedHeaders("*")
모든 Request Header를 허용한다.
.allowedMethods(
"GET",
"POST",
"DELETE",
"PUT",
"PATCH",
"OPTIONS"
)
사용 가능한 HTTP Method를 지정한다.
사용자가 서버로 전달하는 요청 데이터를 담는 DTO이다.
@Builder
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class UserRequestDTO {
private String email;
private String password;
private String name;
}
Lombok을 사용하여 Getter, Builder, Constructor 등을 자동 생성했다.
@Getter
Getter 자동 생성
@Builder
Builder Pattern 사용 가능
@NoArgsConstructor
기본 생성자 자동 생성
@AllArgsConstructor
모든 필드를 받는 생성자 자동 생성
@ToString
객체 정보를 문자열로 출력할 수 있다.
서버에서 클라이언트에게 전달할 사용자 데이터를 담는다.
@Builder
@Getter
@ToString
public class UserResponseDTO {
private String email;
private String password;
private String name;
}
Request DTO와 Response DTO를 분리하여 사용하는 구조를 학습했다.
Client
↓
UserRequestDTO
↓
Controller
↓
Service
↓
DB
↓
UserResponseDTO
↓
Client
실무에서는 보안상 password를 Response DTO에 포함시키지 않는 것이 일반적이다.
MyBatis를 이용하여 DB에 접근하는 Mapper Interface이다.
@Mapper
public interface UserMapper {
public int save(UserRequestDTO request);
public Optional<UserResponseDTO> signIn(
UserRequestDTO request
);
}
@Mapper@Mapper
MyBatis가 해당 Interface를 Mapper로 인식하도록 한다.
직접 구현 클래스를 작성하지 않아도 MyBatis가 XML Mapper와 연결하여 실행한다.
UserMapper.java
public int save(UserRequestDTO request);
userMapper.xml
<insert id="save">
id와 메서드 이름을 동일하게 작성하여 연결한다.
로그인도 동일하다.
public Optional<UserResponseDTO> signIn(
UserRequestDTO request
);
<select id="signIn">
XML Mapper의 가장 중요한 설정 중 하나이다.
<mapper
namespace=
"com.example.inspire_mybatis.features.users.repository.UserMapper">
namespace에는 연결할 Mapper Interface의 전체 패키지 경로를 입력한다.
즉,
UserMapper.java
↕
userMapper.xml
을 연결해 주는 역할을 한다.
회원가입에서는 INSERT를 사용한다.
<insert
id="save"
parameterType=
"com.example.inspire_mybatis.features.users.domain.dto.UserRequestDTO">
INSERT INTO SPRING_USER_TBL(
EMAIL,
PASSWORD,
NAME
)
VALUES(
#{email},
#{password},
#{name}
)
</insert>
DTO의 필드는 다음과 같이 SQL에 전달한다.
request.email
↓
#{email}
request.password
↓
#{password}
request.name
↓
#{name}
#{}MyBatis에서는 다음과 같이 DTO의 값을 SQL Parameter로 전달할 수 있다.
#{email}
예를 들어
UserRequestDTO request
에 다음 값이 들어있다면,
email = test@test.com
password = 1234
name = TEST
MyBatis에서는 다음 SQL에 값을 Binding한다.
INSERT INTO SPRING_USER_TBL(
EMAIL,
PASSWORD,
NAME
)
VALUES(
#{email},
#{password},
#{name}
);
회원가입 API는 다음과 같다.
POST /users/signUp
전체 처리 과정은
React
↓
POST /users/signUp
↓
UserController
↓
UserService.signUp()
↓
UserMapper.save()
↓
userMapper.xml
↓
INSERT
↓
SPRING_USER_TBL
이다.
@PostMapping("/signUp")
public ResponseEntity<?> signUp(
@RequestBody UserRequestDTO request
) {
int signUpFlag = userService.signUp(request);
if(signUpFlag != 0) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(null);
} else {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(null);
}
}
MyBatis의 INSERT 결과값으로 영향을 받은 Row의 개수를 받을 수 있다.
INSERT 성공
→ 1
INSERT 실패
→ 0
따라서
if(signUpFlag != 0)
로 회원가입 성공 여부를 판단했다.
Spring에서 HTTP Response 전체를 직접 제어할 때 사용한다.
ResponseEntity
다음을 설정할 수 있다.
HTTP Status
HTTP Header
HTTP Body
예를 들어 회원가입 성공 시
return ResponseEntity
.status(HttpStatus.CREATED)
.body(null);
결과는
HTTP/1.1 201 Created
가 된다.
이번 실습에서 사용한 주요 Status Code는 다음과 같다.
200 OK
요청 성공
201 Created
리소스 생성 성공
400 Bad Request
잘못된 요청
500 Internal Server Error
서버 내부 오류
회원가입 성공에서는
HttpStatus.CREATED
로그인 성공에서는
HttpStatus.OK
를 사용했다.
로그인은 Email과 Password가 모두 일치하는 사용자를 조회하는 방식으로 구현했다.
<select
id="signIn"
parameterType=
"com.example.inspire_mybatis.features.users.domain.dto.UserRequestDTO"
resultType=
"com.example.inspire_mybatis.features.users.domain.dto.UserResponseDTO">
SELECT
EMAIL,
PASSWORD,
NAME
FROM
SPRING_USER_TBL
WHERE
EMAIL = #{email}
AND PASSWORD = #{password}
</select>
Controller에서 Email과 Password를 받는다.
@RequestParam("email")
String email,
@RequestParam("password")
String password
그리고 Builder를 사용하여 DTO를 생성한다.
UserRequestDTO.builder()
.email(email)
.password(password)
.build();
즉 다음과 같은 형태다.
email
password
↓
UserRequestDTO
↓
UserService.signIn()
로그인 Mapper의 반환 타입으로 Optional을 사용했다.
Optional<UserResponseDTO> signIn(
UserRequestDTO request
);
DB에서 사용자가 조회될 수도 있고 조회되지 않을 수도 있기 때문이다.
사용자 있음
→ Optional<UserResponseDTO>
사용자 없음
→ Optional.empty()
orElseThrow()Service에서는 다음 코드를 사용했다.
UserResponseDTO response =
userMapper
.signIn(request)
.orElseThrow(
() -> new RuntimeException("로그인 실패")
);
사용자가 존재하면 UserResponseDTO를 반환한다.
사용자가 존재하지 않으면
RuntimeException
을 발생시킨다.
전체 흐름은
UserMapper.signIn()
↓
Optional
↓
사용자 존재?
┌─────┴─────┐
YES NO
↓ ↓
DTO Exception
이다.
향후 로그인 기능에서는 다음 개념이 중요하다.
사용자가 누구인지 확인하는 과정
"당신이 누구인가?"
예:
Email + Password
→ 사용자 확인
로그인한 사용자가 특정 기능을 사용할 권한이 있는지 확인하는 과정
"당신이 이 기능을 사용할 권한이 있는가?"
JWT는
JSON Web Token
의 약자로 사용자 인증 정보를 Token 형태로 전달하는 방식이다.
이번 프로젝트에서는 아직 실제 JWT Token을 생성하는 단계가 아니라 JWT 구조를 먼저 구현했다.
@Component
public class JwtProvider {
public String createAT(String email) {
return "Bearer XXXXXXX";
}
public String createRT(String email) {
return "xxxxxxxxxxxxxxxx";
}
}
AT
=
Access Token
로그인 이후 API 요청 시 사용자의 인증 정보를 확인하기 위해 사용한다.
일반적으로 HTTP Header에 다음과 같이 전달한다.
Authorization: Bearer JWT_TOKEN
RT
=
Refresh Token
Access Token이 만료되었을 때 새로운 Access Token을 발급받기 위해 사용한다.
개념적인 구조는
로그인
↓
Access Token
Refresh Token
↓
Access Token을 이용하여 API 호출
↓
Access Token 만료
↓
Refresh Token 확인
↓
새로운 Access Token 발급
이다.
Service에서 로그인 성공 후 Token을 발급한다.
String at =
jwtProvider.createAT(request.getEmail());
String rt =
jwtProvider.createRT(request.getEmail());
그리고 사용자 정보와 Token을 Map에 저장한다.
Map<String, Object> map =
new HashMap<>();
map.put("response", response);
map.put("at", at);
map.put("rt", rt);
구조는 다음과 같다.
Map
response
→ UserResponseDTO
at
→ Access Token
rt
→ Refresh Token
Controller에서는 Token을 HTTP Response Header에 담아 반환한다.
HttpHeaders headers =
new HttpHeaders();
headers.add(
"Authorization",
(String) map.get("at")
);
headers.add(
"Refresh-Token",
(String) map.get("rt")
);
클라이언트는 다음과 같은 응답을 받을 수 있다.
HTTP/1.1 200 OK
Authorization: Bearer XXXXXXX
Refresh-Token: xxxxxxxxxxxxxxxx
Body에는 사용자 정보를 반환한다.
.body(
(UserResponseDTO) map.get("response")
);
다음 설정도 추가했다.
headers.add(
"Access-Control-Expose-Headers",
"Authorization, Refresh-Token"
);
브라우저에서는 기본적으로 모든 Response Header를 JavaScript에서 읽을 수 있는 것은 아니다.
따라서 React에서
Authorization
Refresh-Token
Header를 읽을 수 있도록 해당 Header를 노출시킨다.
.envDB 계정이나 Password와 같은 중요한 설정값을 코드에 직접 작성하지 않고 별도 환경변수 파일에서 관리한다.
현재 .env에는 다음과 같은 내용이 필요하다.
DB_DRIVER=org.mariadb.jdbc.Driver
DB_URL=jdbc:mariadb://localhost:3306/lgcns
DB_USER=root
DB_PASSWORD=본인의_DB_비밀번호
예:
DB_DRIVER=org.mariadb.jdbc.Driver
DB_URL=jdbc:mariadb://localhost:3306/lgcns
DB_USER=root
DB_PASSWORD=1234
.env를 사용하는 이유다음과 같은 정보를 Source Code 내부에 직접 작성하면 보안상 좋지 않다.
DB Password
API KEY
JWT Secret Key
Cloud Access Key
따라서 환경변수로 분리한다.
.env
↓
환경변수
↓
application-dev.yml
↓
Spring Boot
Git을 사용할 경우 .env는 Repository에 올리지 않는 것이 중요하다.
.gitignore
.env
.env 설정을 Spring의 설정 파일에서 사용할 수 있도록 애플리케이션 시작 시 환경변수로 등록했다.
Dotenv env =
Dotenv.configure()
.ignoreIfMissing()
.load();
그리고
env.entries().forEach(entry -> {
System.setProperty(
entry.getKey(),
entry.getValue()
);
});
를 이용하여 .env에 있는 값을 Java System Property에 등록한다.
전체 흐름은
.env
↓
Dotenv
↓
System.setProperty()
↓
${DB_URL}
${DB_USER}
${DB_PASSWORD}
↓
application-dev.yml
↓
DataSource
↓
MariaDB
이다.
기본 Profile을 설정한다.
spring:
profiles:
default: dev
별도의 Profile을 지정하지 않으면 dev 환경을 사용한다.
개발환경 설정 파일이다.
server:
port: 8000
spring:
config:
activate:
on-profile: dev
datasource:
driver-class-name: ${DB_DRIVER}
url: ${DB_URL}
username: ${DB_USER}
password: ${DB_PASSWORD}
mybatis:
mapper-locations: classpath:/mappers/**/*Mapper.xml
운영환경에서 사용할 설정 파일이다.
spring:
config:
activate:
on-profile: prod
현재는 기본적인 Profile 구조만 생성한 단계이다.
향후에는 개발환경과 실제 운영환경의 DB 설정 등을 각각 다르게 설정할 수 있다.
application.yml
↓
┌──────────────┐
│ │
dev prod
│ │
↓ ↓
개발환경 운영환경
@Operation(
summary = "회원가입",
description = "신규가입(email, password, name)"
)
API 결과 Status도 정의했다.
@ApiResponses({
@ApiResponse(
responseCode = "201",
description = "가입성공"
),
@ApiResponse(
responseCode = "400",
description = "유효성검사실패"
),
@ApiResponse(
responseCode = "500",
description = "가입실패"
)
})
@Operation(
summary = "로그인",
description = "사용자 로그인(email, password)"
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그인 성공"
),
@ApiResponse(
responseCode = "400",
description = "로그인 실패"
)
})
Swagger는 다음 URL에서 확인할 수 있다.
http://localhost:8000/swagger-ui/index.html
25일차에는 주로 Spring Boot와 MyBatis가 연결되는 기본 구조를 학습했다.
Request
↓
Controller
↓
Service
↓
Mapper
↓
MyBatis XML
↓
Database
핵심은
Spring MVC
+
MyBatis
의 동작 원리를 이해하는 것이었다.
26일차에는 25일차 구조를 그대로 활용하면서 실제 서비스 기능을 추가했다.
React
↓
CORS
↓
Controller
↓
Service
├────────→ Mapper
│ ↓
│ MyBatis XML
│ ↓
│ DB
│
└────────→ JwtProvider
↓
Access Token
Refresh Token
즉 단순히 Spring과 MyBatis의 연결 여부를 확인하는 단계에서 실제 웹 서비스의 인증 흐름을 구현하는 단계로 발전했다.
| 구분 | 25일차 testcase | 26일차 inspire_mybatis |
|---|---|---|
| 목적 | Spring + MyBatis 구조 이해 | 실제 회원 기능 구현 |
| Controller | 요청 처리 학습 | 회원가입 / 로그인 API |
| Service | 기본 계층 연결 | 로그인 검증 및 Token 발급 |
| Mapper | MyBatis 연결 | 회원 저장 / 로그인 조회 |
| DTO | 요청 데이터 전달 | Request / Response 역할 분리 |
| Swagger | API 명세 학습 | 실제 회원 API 명세 |
| CORS | 없음 | React 연동 |
| JWT | 없음 | AT / RT 구조 추가 |
| HTTP Header | 기본 응답 | Token 전달 |
| 환경변수 | 기본 설정 | .env 도입 |
| Profile | 단순 설정 | dev / prod 환경 분리 |
| 프로젝트 성격 | Test Case | 실제 서비스 구조 |
namespace, id를 이용하여 연결된다.INSERT 실행 결과로 영향을 받은 Row의 수를 받을 수 있다.Optional을 이용하면 조회 결과가 존재하지 않는 상황을 명확하게 처리할 수 있다.ResponseEntity를 사용하면 HTTP Status, Header, Body를 모두 직접 제어할 수 있다.Access-Control-Expose-Headers 설정이 필요할 수 있다..env를 이용하면 DB Password 같은 민감한 설정을 Source Code에서 분리할 수 있다.처음에는 25일차 testcase와 26일차 프로젝트가 구조적으로 크게 달라 보이지 않았다.
하지만 실제로는 기본적인 계층 구조는 동일하다.
Controller
↓
Service
↓
Mapper
↓
DB
차이는 이 구조를 이용하여 실제 서비스 기능을 붙였다는 점이다.
26일차에는
회원가입
로그인
JWT
CORS
HTTP Header
환경변수
Profile
등이 추가되었다.
즉 새로운 구조를 처음부터 학습한 것이 아니라,
25일차에 만든 Spring + MyBatis 구조를 실제 서비스 형태로 확장한 것
이라고 이해할 수 있다.
현재는 아니다.
public String createAT(String email) {
return "Bearer XXXXXXX";
}
현재 JwtProvider는 실제 JWT를 만드는 것이 아니라 임시 문자열을 반환한다.
따라서 현재 단계는
로그인 성공
↓
Token Provider 호출
↓
Access Token
Refresh Token
↓
Client 반환
이라는 인증 구조를 먼저 학습하는 단계라고 이해했다.
현재 SQL에서는
WHERE EMAIL = #{email}
AND PASSWORD = #{password}
로 Password를 직접 비교하고 있다.
학습 단계에서는 로그인 흐름을 이해하기 위한 구현이지만 실제 서비스에서는 Password를 평문으로 저장하지 않는다.
일반적으로 BCrypt와 같은 단방향 Hash를 사용하여 Password를 관리한다.
CorsConfig 생성localhost:3000 Origin 허용JwtProvider 생성.env 환경변수 사용 구조 확인.env 값 System Property 등록application-dev.yml 작성application-prod.yml 작성회원가입 요청 시
POST /users/signUp
요청이
Controller
↓
Service
↓
Mapper
↓
MyBatis XML
↓
SPRING_USER_TBL
순서로 전달되어 사용자 정보가 저장되는 구조를 구현했다.
로그인 시에는
GET /users/signIn
↓
UserController
↓
UserService
↓
UserMapper
↓
DB 사용자 확인
↓
JwtProvider
↓
Access Token / Refresh Token 생성
↓
HTTP Header
↓
UserResponseDTO 반환
의 흐름을 구현했다.
.env에는 어떤 내용을 작성해야 하는가?application-dev.yml에서는 다음 환경변수를 참조하고 있었다.
driver-class-name: ${DB_DRIVER}
url: ${DB_URL}
username: ${DB_USER}
password: ${DB_PASSWORD}
하지만 실제 값을 어디에 작성해야 하는지 확인이 필요했다.
프로젝트 Root에 .env 파일을 두고 다음과 같이 설정한다.
DB_DRIVER=org.mariadb.jdbc.Driver
DB_URL=jdbc:mariadb://localhost:3306/lgcns
DB_USER=root
DB_PASSWORD=본인의_DB_비밀번호
그리고 InspireMybatisApplication에서 Dotenv를 이용하여 값을 읽는다.
Dotenv env =
Dotenv.configure()
.ignoreIfMissing()
.load();
env.entries().forEach(entry -> {
System.setProperty(
entry.getKey(),
entry.getValue()
);
});
이후 Spring에서는
${DB_URL}
${DB_USER}
${DB_PASSWORD}
형태로 해당 값을 사용할 수 있다.
또한 .env에는 민감한 정보가 포함될 수 있기 때문에 .gitignore에 추가한다.
.env
[ React ]
localhost:3000
│
│ HTTP Request
↓
[ CorsConfig ]
↓
[ UserController ]
↓
[ UserService ]
┌──┴─────────────┐
│ │
↓ ↓
[UserMapper] [JwtProvider]
│ │
↓ ├─ Access Token
[userMapper.xml] └─ Refresh Token
│
↓
[MariaDB]
↓
ResponseEntity
Status
+
Headers
+
Body
↓
[ React ]
25일차
Spring Boot
↓
Controller
↓
DTO
↓
Service
↓
Mapper
↓
MyBatis XML
↓
Database
↓ 확장
26일차
React
↓
CORS
↓
Controller
↓
Service
├───────────────┐
↓ ↓
Mapper JwtProvider
↓ ↓
MyBatis XML AT / RT
↓
Database
+
ResponseEntity
+
HTTP Status
+
HTTP Header
+
Swagger
+
.env
+
Spring Profile
25일차에는 Spring Boot에서 Controller → Service → Mapper → MyBatis XML → DB로 이어지는 기본적인 백엔드 계층 구조를 이해하는 것이 중심이었다.
26일차에는 이 구조를 그대로 활용하여 실제 회원가입과 로그인 기능을 구현했다.
특히 로그인 성공 이후 JWT Token을 발급한다는 개념이 등장하면서 단순한 DB CRUD를 넘어 사용자 인증(Authentication) 영역으로 학습 범위가 확장되었다.
또한 React와 Spring을 연결하기 위한 CORS, HTTP Header를 통한 Token 전달, .env를 통한 민감정보 분리, Spring Profile을 통한 개발환경과 운영환경 분리까지 학습하면서 프로젝트가 단순한 Test Case에서 실제 웹 서비스에 가까운 구조로 발전했다.
26일차의 핵심은 새로운 Spring 구조를 다시 배우는 것이 아니라,
25일차에 배운 Spring + MyBatis 기본 구조 위에 회원가입, 로그인, JWT, CORS, 환경설정을 추가하여 실제 서비스 구조로 확장하는 과정
이라고 정리할 수 있다.