개발 환경 및 아키텍처 개요
본 프로젝트는 Spring Cloud를 활용한 마이크로서비스 아키텍처(MSA)를 기반으로 설계되었습니다. 시스템은 크게 서비스 레지스트리(Eureka), API 게이트웨이, 그리고 비즈니스 로직을 처리하는 사용자 서비스와 상품 서비스로 분리됩니다. 각 서비스는 독립적인 데이터베이스를 소유하며, 서비스 간 통신은 Feign 클라이언트를 통해 이루어집니다.
- JDK: 1.8 이상
- 빌드 도구: Maven 3.6+
- 데이터베이스: MySQL 8.0
- Spring Boot: 2.3.x
- Spring Cloud: Hoxton.SR8
데이터베이스 스키마 설계
사용자 모듈과 상품 모듈은 각각 독립적인 데이터베이스를 사용합니다. 기존 설계에서 컬럼명을 표준화하고 정규화를 고려하여 스키마를 재설계했습니다.
사용자 데이터베이스 (ecommerce_user)
CREATE DATABASE ecommerce_user CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE ecommerce_user;
CREATE TABLE users (
user_id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
gender VARCHAR(10),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (username, password_hash, gender) VALUES
('alice', 'hashed_pw_1', 'F'),
('bob', 'hashed_pw_2', 'M'),
('charlie', 'hashed_pw_3', 'M');
상품 데이터베이스 (ecommerce_product)
CREATE DATABASE ecommerce_product CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE ecommerce_product;
CREATE TABLE products (
product_id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL
);
INSERT INTO products (product_name, description, price) VALUES
('Strawberry', 'Fresh organic strawberries', 4.50),
('Apple', 'Crisp red apples', 2.00),
('Sparkling Water', 'Refreshing carbonated water', 1.50);
서비스 레지스트리(Eureka Server) 구축
마이크로서비스의 핵심인 서비스 디스커버리를 위해 Eureka Server를 구성합니다. 보안을 위해 Spring Security를 연동하여 대시보드 접근을 제어합니다.
의존성 및 설정
pom.xml에 Eureka Server와 Security 스타터를 추가합니다.
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
application.yml 파일을 통해 포트를 지정하고, Eureka 서버 자체는 다른 레지스트리에 등록되지 않도록 설정합니다. 인코딩 관련 오류를 방지하기 위해 반드시 UTF-8로 저장해야 합니다.
server:
port: 8761
spring:
security:
user:
name: admin
password: admin123
eureka:
client:
register-with-eureka: false
fetch-registry: false
보안 설정 및 실행
CSRF 보호로 인한 클라이언트 등록 오류를 방지하기 위해 Security 설정 클래스에서 CSRF를 비활성화하거나 특정 엔드포인트를 허용하도록 구성합니다.
@Configuration
@EnableWebSecurity
public class EurekaSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
}
}
메인 클래스에 @EnableEurekaServer 어노테이션을 추가하여 서버를 활성화합니다.
공통 모듈(Common) 및 엔티티 정의
프로바이더와 컨슈머 간의 데이터 전송 객체(DTO) 및 엔티티 중복을 피하기 위해 공통 모듈을 생성합니다. Lombok을 활용하여 보일러플레이트 코드를 줄입니다.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private Integer userId;
private String username;
private String passwordHash;
private String gender;
}
공통 모듈은 mvn clean install 명령어를 통해 로컬 Maven 저장소에 JAR로 배포합니다. 테스트 코드로 인한 빌드 실패를 방지하려면 maven-surefire-plugin에서 테스트 스킵 옵션을 고려할 수 있습니다.
사용자 서비스 프로바이더(User Provider) 구현
실제 비즈니스 로직과 데이터베이스 연동을 담당하는 프로바이더 서비스를 구현합니다. MyBatis를 사용하여 데이터 접근 계층을 구성합니다.
데이터 접근 계층 (MyBatis)
XML 매핑 파일 대신 어노테이션 방식을 사용하여 코드의 응집도를 높였습니다.
@Mapper
public interface UserRepository {
@Select("SELECT * FROM users WHERE username = #{username} AND password_hash = #{password}")
UserDto findByCredentials(@Param("username") String username, @Param("password") String password);
@Insert("INSERT INTO users (username, password_hash, gender) VALUES (#{username}, #{password}, #{gender})")
int saveUser(UserDto user);
}
컨트롤러 및 서비스
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository;
@PostMapping("/login")
public ResponseEntity<UserDto> login(@RequestBody UserDto request) {
UserDto user = userRepository.findByCredentials(request.getUsername(), request.getPasswordHash());
return user != null ? ResponseEntity.ok(user) : ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody UserDto request) {
int result = userRepository.saveUser(request);
return result > 0 ? ResponseEntity.ok("Success") : ResponseEntity.badRequest().body("Failed");
}
}
메인 클래스에 @EnableEurekaClient를 추가하여 서비스 레지스트리에 등록되도록 합니다.
사용자 서비스 컨슈머(User Consumer) 및 Feign 클라이언트
컨슈머는 프론트엔드 요청을 받아 프로바이더로 라우팅하는 BFF(Backend for Frontend) 역할을 수행합니다. OpenFeign을 사용하여 선언적 REST 클라이언트를 구성하고, 폴백(Fallback)을 통해 서킷 브레이커 패턴을 적용합니다.
Feign 클라이언트 및 폴백(Fallback)
@FeignClient(name = "user-provider", fallback = UserClientFallback.class)
public interface UserClient {
@PostMapping("/api/users/login")
UserDto login(@RequestBody UserDto user);
@PostMapping("/api/users/register")
String register(@RequestBody UserDto user);
}
@Component
public class UserClientFallback implements UserClient {
@Override
public UserDto login(UserDto user) {
return new UserDto();
}
@Override
public String register(UserDto user) {
return "Service is currently unavailable. Please try again later.";
}
}
웹 컨트롤러 및 Thymeleaf 연동
@Controller를 사용하여 뷰 이름을 반환하고, Thymeleaf 템플릿 엔진을 통해 HTML을 렌더링합니다.
@Controller
@RequestMapping("/web")
public class WebController {
@Autowired
private UserClient userClient;
@GetMapping("/login")
public String showLoginPage() {
return "login";
}
@PostMapping("/login")
public String processLogin(@ModelAttribute UserDto user, Model model) {
UserDto result = userClient.login(user);
if (result.getUserId() != null) {
model.addAttribute("username", result.getUsername());
return "welcome";
}
model.addAttribute("error", "Invalid credentials");
return "login";
}
}
인증 및 회원 가입 프론트엔드 구현
Thymeleaf를 활용하여 로그인 및 회원가입 폼을 구현합니다. 클라이언트 측에서 기본적인 유효성 검사를 수행하도록 JavaScript를 추가합니다.
로그인 페이지 (login.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>사용자 로그인</title>
</head>
<body>
<h2>로그인</h2>
<form th:action="@{/web/login}" method="post">
<label>아이디:</label>
<input type="text" name="username" required /><br/>
<label>비밀번호:</label>
<input type="password" name="passwordHash" required /><br/>
<button type="submit">로그인</button>
</form>
<p th:if="${error}" th:text="${error}" style="color:red;"></p>
<a th:href="@{/web/register}">회원가입으로 이동</a>
</body>
</html>
회원가입 페이지 (register.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>회원가입</title>
<script>
function validateForm() {
var pw = document.getElementById("pw").value;
var confirmPw = document.getElementById("confirmPw").value;
if (pw !== confirmPw) {
alert("비밀번호가 일치하지 않습니다.");
return false;
}
return true;
}
</script>
</head>
<body>
<h2>회원가입</h2>
<form th:action="@{/web/register}" method="post" onsubmit="return validateForm()">
<label>아이디:</label>
<input type="text" name="username" required /><br/>
<label>비밀번호:</label>
<input type="password" id="pw" name="passwordHash" required /><br/>
<label>비밀번호 확인:</label>
<input type="password" id="confirmPw" required /><br/>
<label>성별:</label>
<input type="text" name="gender" /><br/>
<button type="submit">가입하기</button>
</form>
</body>
</html>
모든 마이크로서비스(Eureka Server, User Provider, User Consumer)를 순차적으로 기동한 후, 컨슈머 서버의 포트로 접근하여 웹 인터페이스와 서비스 간 통신이 정상적으로 이루어지는지 검증합니다.