클린아키텍처 자체가 추상적인 개념으로 설명하는 것이다 보니
클린아키텍처의 규칙을 지키면서 구현하는 방법은 많습니다.
회사의 인원, 서비스 성숙도에 따라 적절히 적용하시면 됩니다.
특히 인원이 적어서 개발이 바쁜 와중에 클린아키텍처를 따라서
너무 타이트하게 나누자고 하면 러닝 커브와 많아진 파일로
팀원들의 반발심만 부추길 것입니다.
차근 차근 도입하면서 설득하는 것이 중요합니다.
이번 시간에는 그중 클린아키텍처를 처음 접하는 사람도 쉽게 도입이 가능한
엔티티와 도메인을 합쳐서 처리하는 방법으로 만들어진 프로젝트를 설명드리겠습니다.
(제 프로젝트가 정답은 아니니 참고만 하시고, 틀린게 있다면 지적해주세요)
GitHub dev



다른 도메인의 설명은 클린아키텍처 설명시 중복되니 Order 도메인만 설명하겠습니다.
나머지 코드는 상단 Github링크에서 확인 부탁드립니다.

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(force = true)
@Schema(description = "포인트 충전 요청 객체")
public class PutUserChargePointRequest {
@Schema(description = "사용자 식별자(아이디)")
@Positive(message = "사용자 아이디는 양수입니다.")
@NotNull(message = "사용자 아이디는 필수값입니다.")
private final Long id;
@Schema(description = "충전할 포인트")
@Positive(message = "충전 포인트는 양수입니다.")
@NotNull(message = "충전 포인트는 필수값입니다.")
private final Integer point;
}
@Getter
@NoArgsConstructor(force = true)
@Schema(description = "포인트 충전 응답 객체")
public class PutUserChargePointResponse {
@Schema(description = "사용자")
private final UserDto user;
public PutUserChargePointResponse(UserDto user){
this.user = user;
}
}
@RequiredArgsConstructor
@RestController
public class UserController implements UserControllerDoc {
private final UserService userService;
public static final String API_PUT_USER_POINT = "/api/user/point";
@Override
@PutMapping(API_PUT_USER_POINT)
public ApiResponse<PutUserChargePointResponse> userPoint(
@Valid @RequestBody PutUserChargePointRequest putUserChargePointRequest) throws Exception {
return ApiResponse.ok(
new PutUserChargePointResponse(
userService.chargePoint(putUserChargePointRequest.getId(),
putUserChargePointRequest.getPoint())
)
);
}
}

@Getter
@Schema(description = "사용자")
public class UserDto {
@Schema(description = "사용자 식별자(아이디)")
private final Long id;
@Schema(description = "사용자 이름")
private String name;
@Schema(description = "현재 포인트")
private final Integer point;
@Builder
private UserDto(Long id, String name, Integer point) {
this.id = id;
this.name = name;
this.point = point;
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final UserPointTransactionRepository userPointTransactionRepository;
@Transactional
@DistributedLock("#userId")
public UserDto chargePoint(Long userId, Integer point) throws Exception{
final UserEntity user = userRepository.findById(userId).orElseThrow(
() -> new IllegalArgumentException("존재하지 않는 사용자입니다."));
user.chargePoint(point);
userRepository.save(user);
userPointTransactionRepository.save(UserPointTransactionEntity.createByCharge(user,point));
return user.toDto();
}
}

@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@ToString
@Table(name = "TB_USER")
public class UserEntity extends BaseEntity implements Comparable<UserEntity> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 30)
private String name;
@Column
@ColumnDefault("0")
private Integer point = 0;
@ToString.Exclude
@OneToMany(mappedBy = "user")
private SortedSet<UserPointTransactionEntity> userPointTransactions = new TreeSet<>();
@Builder
private UserEntity(Long id, String name, Integer point) {
this.id = id;
this.name = name;
this.point = point == null ? 0 : point;
}
@Override
public int compareTo(UserEntity o) {
return 1;
}
public void chargePoint(Integer point) {
this.point += point;
}
public void usePoint(Integer point) {
if(this.point - point < 0) throw new IllegalArgumentException("포인트가 부족합니다");
this.point -= point;
}
public UserDto toDto() {
return UserDto.builder()
.id(this.getId())
.name(this.getName())
.point(this.getPoint())
.build();
}
}
해당 코드가 마음에 들지 않으실수 있습니다.
다음 시간에는 좀 더 타이트한 구현 프로젝트를 가지고 설명하겠습니다.