외부 계층(Outer Layers): 가장 바깥쪽에 위치하며, 사용자 인터페이스, 웹 API, 데이터베이스 등 외부 요소와 상호 작용합니다. 이 계층은 시스템의 외부 요구사항을 처리하고 사용자의 입력을 받아들이며, 비즈니스 로직을 직접적으로 포함하지 않습니다.
응용 계층(Application Layer): 외부 계층과 도메인 계층 사이에 위치하며, 비즈니스 규칙과 흐름을 담당합니다. 사용자의 요청을 받아들이고 비즈니스 로직을 실행하며, 도메인 객체와 상호 작용합니다. 응용 계층은 도메인의 상태를 변경하거나 도메인 객체의 상태를 조회하기 위해 도메인 계층에 의존합니다.
도메인 계층(Domain Layer): 가장 안쪽에 위치하며, 시스템의 핵심 비즈니스 규칙과 개념을 포함합니다. 도메인 객체와 도메인 서비스를 정의하고, 비즈니스 로직을 실행합니다. 도메인 계층은 외부 계층과 응용 계층에 대한 의존성이 없어야 하며, 도메인의 핵심 개념을 가장 순수한 형태로 표현합니다.
인터페이스 어댑터 계층(Interface Adapters): 외부 계층과 도메인 계층 사이에 위치하며, 각 계층 간의 데이터 변환, 형식 변환, 외부 인터페이스 구현 등을 처리합니다. 이 계층은 외부 요소와 시스템 내부의 계층을 분리하고, 각 계층이 독립적으로 변경될 수 있도록 유연성을 제공합니다.
※ 참고 : 클린 아키텍처 책
@Value
@EqualsAndHashCode(callSuper = false)
public class RegisterUserRequest extends SelfValidating<RegisterUserRequest> {
@Pattern(
regexp = "^[a-z0-9]{4,20}$",
message = "아이디는 영어 소문자와 숫자만 사용하여 4~20자리여야 합니다."
)
@NotBlank(message = "아이디은 필수 입력 값입니다.")
private String username;
@Pattern(
regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,16}",
message = "비밀번호는 숫자,문자,특수문자를 포함한 6~18로 입력해주세요."
)
@NotBlank(message = "비밀번호는 필수 입력 값입니다.")
private String password;
public RegisterUserRequest(
String username,
String password,
) {
this.username = username;
this.password = password;
this.validateSelf();
}
}
this.validateSelf();
사용하여 유효성 검사를 실행한다.public abstract class SelfValidating<T> {
private Validator validator;
public SelfValidating() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
protected void validateSelf() {
Set<ConstraintViolation<T>> violations = validator.validate((T) this);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}
validator
를 사용하여 객체(this
)의 속성에 대한 유효성을 평가하고, 어긴 항목이 있다면 ConstraintViolationException
예외를 throw합니다.@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserRegisterController {
private final RegisterUserUseCase registerUserUseCase;
private final UserResponseMapper userResponseMapper;
@PostMapping("/register")
public ResponseEntity<ReturnObject> registerUser(
@RequestBody RegisterUserRequest registerUserRequest
) {
RegisterUserCommand command = RegisterUserCommand.builder()
.username(registerUserRequest.getUsername())
.password(registerUserRequest.getPassword())
.build();
User user = registerUserUseCase.registerUser(command);
RegisterUserResponse response = userResponseMapper.mapToRegisterUserResponse(user);
ReturnObject returnObject = ReturnObject.builder()
.success(true)
.data(response)
.build();
return ResponseEntity.status(HttpStatus.OK).body(returnObject);
}
}
@Builder
@Data
public class RegisterUserCommand {
private String username;
private String password;
public User toEntity() {
return User.builder()
.username(username)
.password(password)
.build();
}
}
public interface RegisterUserUseCase {
User registerUser(RegisterUserCommand command);
}
@Slf4j
@UseCase
@Transactional
@RequiredArgsConstructor
public class RegisterUserService implements RegisterUserUseCase {
private final SaveUserStatePort saveUserStatePort;
private final PasswordEncoder passwordEncoder;
@Override
@Transactional
public User registerUser(RegisterUserCommand command) {
if (!Objects.equals(command.getPassword(), command.getConfirmPassword())) {
throw new RuntimeException("두개의 비밀번호가 맞지 않습니다.");
}
User saveUser = command.toEntity(passwordEncoder);
saveUserStatePort.saveUser(saveUser);
return saveUser;
}
}
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class User extends BaseTimeEntity implements Serializable {
private Long userId;
private String username;
private String password;
public UserJpaEntity toJpaEntity() {
return UserJpaEntity.builder()
.id(userId)
.username(username)
.password(password)
.build();
}
}
@AllArgsConstructor(access = AccessLevel.PRIVATE)
를 사용하는 이유는public interface SaveUserStatePort {
void saveUser(User user);
}
@RequiredArgsConstructor
@PersistenceAdapter
public class UserPersistenceAdapterState implements SaveUserStatePort {
private final UserJpaRepo userJpaRepo;
private final UserPersistenceMapper userPersistenceMapper;
@Override
@Transactional
public void saveUser(User user) {
userJpaRepo.save(userPersistenceMapper.mapToJpaEntity(user));
}
}
@Component
public class UserPersistenceMapper {
public User mapToDomainEntity(UserJpaEntity userJpaEntity) {
return User.builder()
.userId(userJpaEntity.getId())
.username(userJpaEntity.getUsername())
.password(userJpaEntity.getPassword())
.nickname(userJpaEntity.getNickname())
.phone(userJpaEntity.getPhone())
.email(userJpaEntity.getEmail())
.role(userJpaEntity.getRole())
.build();
}
public UserJpaEntity mapToJpaEntity(User user) {
return user.toJpaEntity();
}
}
mapToDomainEntity
→ 클래스 명 그대로 UserJpaEntity를 User로 변환해주는 역할을 한다.mapToJpaEntity
→ 클래스 명 그대로 User를 UserJpaEntity로 변환해주는 역할을 한다.UpdateUserStatePort
, LoadUserPort
두개를 비교하자면 StatePort, Port로 구분이 가능한데 State가 붙은 클래스명은 상태 변화가 존재하여 클래스 명에 기입을 해주었습니다.username
, nickname
은 하나의 Controller에 담아서 사용을 하였습니다.UpdateUserStatePort
, DeleteUserStatePort
, ActivateUserStatePort
등으로 사용한다.LoadUserPort
, SaveUserPort
등으로 사용한다.
클린 아키텍쳐에 대한 개념과 쉽게 풀어낸 설명 덕분에 이해하기 쉬웠습니다!