Skip to content

Commit fa1ca22

Browse files
committed
refactor: 회원 도메인 추가 리팩터링 (#2)
- 엔티티 공용 멤버 관리를 위한 BaseGeneralEntity 클래스 추가 - 기존의 deleteAt 기반 논리적 삭제 로직을 deleteYn 기반으로 변경 - 기타 개선사항 고도화
1 parent 2385695 commit fa1ca22

33 files changed

+267
-169
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ OAuth 2.0 가이드 문서에 JSON 토큰에 대한 설명도 나와 있습니
228228
회원 프로필 테이블입니다. 회원 테이블과 역할이 조금 다릅니다. 회원 테이블이 회원의 권한, 로그인 정보 등의 데이터를 담는다면,
229229
회원 프로필 테이블은 프로필 이미지, 자기소개, 연락처, 이름 등 회원 프로필 데이터를 가지고 있습니다.
230230

231-
- `member_id`
231+
- `id`
232232
- `member` 테이블을 참조하는 FK이자, `member_profile` 테이블의 PK입니다. `member_profile` 테이블과 `member` 테이블은 식별 관계입니다.
233233

234234
### `available_study_time`

docs/extract-member-id/extract-member-id.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class TestController {
5050

5151
```json
5252
{
53-
"member_id": 1,
53+
"id": 1,
5454
"created_at": "2025-05-04 20:24:28.000000",
5555
"updated_at": "2025-05-04 20:24:32.000000",
5656
"auto_matching": "F",
@@ -178,4 +178,4 @@ public class TestController {
178178

179179
## 주의사항
180180

181-
`SecurityContextHolder`, `OAuth2AuthenticationPrincipal` 등은 Spring Security 기술이고, **변할 수 있는 것**입니다. 이 때문에 **Service 객체에 Spring Security 기술을 노출시켜서는 안 됩니다**. 왜냐하면 Service 객체는 비즈니스 로직을 가지고 있는 Application 코드이고, 비즈니스 로직 코드는 요구사항이 변경될 경우를 제외하고 변경되어서는 안 됩니다. 다시 말해, 비즈니스 로직 코드는 외부 기술에 의존해서는 안 된다는 것입니다. Service 객체 안에서 `SecurityContextHolder`에 접근하여 `Authentication` 객체를 꺼내는 코드를 작성한다면 그 Service 객체는 Spring Security라는 기술에 종속됩니다. 웬만하면 계속 Spring Security를 사용하겠지만, 그래도 Spring Security가 아닌 다른 기술을 사용하게 되었다고 가정해 봅시다. 그러면 Spring Security에 종속된 Service 객체의 코드 변경 없이 Spring Security를 대체할 수 없게 됩니다. 저희 프로젝트 규모가 더 커져서 막 코드가 10만 줄이 넘어가는데, Application 코드 곳곳에 Spring Security에 대한 의존성이 발견된다고 합시다. 그러면 이 프로젝트는 최후의 순간까지 Spring Security를 사용해야 합니다. 더 개쩌는 기술이 나온다고 하더라도요.
181+
`SecurityContextHolder`, `OAuth2AuthenticationPrincipal` 등은 Spring Security 기술이고, **변할 수 있는 것**입니다. 이 때문에 **Service 객체에 Spring Security 기술을 노출시켜서는 안 됩니다**. 왜냐하면 Service 객체는 비즈니스 로직을 가지고 있는 Application 코드이고, 비즈니스 로직 코드는 요구사항이 변경될 경우를 제외하고 변경되어서는 안 됩니다. 다시 말해, 비즈니스 로직 코드는 외부 기술에 의존해서는 안 된다는 것입니다. Service 객체 안에서 `SecurityContextHolder`에 접근하여 `Authentication` 객체를 꺼내는 코드를 작성한다면 그 Service 객체는 Spring Security라는 기술에 종속됩니다. 웬만하면 계속 Spring Security를 사용하겠지만, 그래도 Spring Security가 아닌 다른 기술을 사용하게 되었다고 가정해 봅시다. 그러면 Spring Security에 종속된 Service 객체의 코드 변경 없이 Spring Security를 대체할 수 없게 됩니다. 저희 프로젝트 규모가 더 커져서 막 코드가 10만 줄이 넘어가는데, Application 코드 곳곳에 Spring Security에 대한 의존성이 발견된다고 합시다. 그러면 이 프로젝트는 최후의 순간까지 Spring Security를 사용해야 합니다. 더 개쩌는 기술이 나온다고 하더라도요.

init_data/ZTO_LOCAL_DDL.sql

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ SET FOREIGN_KEY_CHECKS = 1;
1818

1919

2020
CREATE TABLE `member` (
21-
`member_id` bigint PRIMARY KEY AUTO_INCREMENT COMMENT '회원 Identifier',
21+
`id` bigint PRIMARY KEY AUTO_INCREMENT COMMENT '회원 Identifier',
2222
`oidc_id` varchar(50) COMMENT 'Open ID Connect에 의한 ID',
2323
`login_id` VARCHAR(50) COMMENT '로그인할 때 사용되는 ID (자체 로그인)',
2424
`member_status` VARCHAR(8) NOT NULL DEFAULT 'ACTIVE' COMMENT '회원 상태',
@@ -29,18 +29,18 @@ CREATE TABLE `member` (
2929
);
3030

3131
CREATE TABLE `role` (
32-
`role_id` varchar(20) PRIMARY KEY COMMENT '권한 ID',
32+
`id` varchar(20) PRIMARY KEY COMMENT '권한 ID',
3333
`role_name` varchar(20) NOT NULL COMMENT '권한명'
3434
);
3535

3636
CREATE TABLE `access_permission` (
37-
`permission_id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '접근허가 ID',
37+
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '접근허가 ID',
3838
`endpoint` VARCHAR(80) NOT NULL COMMENT '엔드포인트',
3939
`http_method` VARCHAR(8) NOT NULL COMMENT 'HTTP 메소드 (Upper Case)' /* CHAR로 변경 고려 */
4040
);
4141

4242
CREATE TABLE `access_permission_role` (
43-
`access_permission_role_id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '접근허가_롤 ID',
43+
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '접근허가_롤 ID',
4444
`permission_id` BIGINT NOT NULL COMMENT '접근허가 참조 FK',
4545
`role_id` VARCHAR(20) NOT NULL COMMENT '권한 참조 FK'
4646
);
@@ -59,15 +59,15 @@ CREATE TABLE `member_profile` (
5959
);
6060

6161
CREATE TABLE `member_interest` (
62-
member_interest_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '관심사 ID',
62+
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '관심사 ID',
6363
name VARCHAR(20) NOT NULL COMMENT '관심사 이름',
6464
member_id BIGINT NOT NULL COMMENT '회원 프로필 참조 FK',
6565
`created_at` timestamp NOT NULL COMMENT '생성시간',
6666
`updated_at` timestamp NOT NULL COMMENT '수정시간'
6767
);
6868

6969
CREATE TABLE `available_study_time` (
70-
available_study_time_id BIGINT PRIMARY KEY COMMENT '스터디 가능 시간대 ID',
70+
id BIGINT PRIMARY KEY COMMENT '스터디 가능 시간대 ID',
7171
from_time TIME(6) COMMENT '스터디 가능 시작 시간 (날짜 빼고 시간만 사용)',
7272
to_time TIME(6) COMMENT '스터디 가능 끝 시간 (날짜 빼고 시간만 사용)',
7373
label VARCHAR(10) NOT NULL COMMENT '시간대 라벨 (오전, 오후, 저녁 등등)',
@@ -76,21 +76,21 @@ CREATE TABLE `available_study_time` (
7676
);
7777

7878
CREATE TABLE `profile_avl_time` (
79-
available_study_time_id BIGINT NOT NULL COMMENT '스터디 가능 시간대 참조 FK',
79+
id BIGINT NOT NULL COMMENT '스터디 가능 시간대 참조 FK',
8080
member_id BIGINT NOT NULL COMMENT '회원 프로필 참조 FK',
8181
PRIMARY KEY (available_study_time_id, member_id)
8282
);
8383

8484
CREATE TABLE `image` (
85-
`image_id` bigint PRIMARY KEY AUTO_INCREMENT COMMENT '이미지 ID',
85+
`id` bigint PRIMARY KEY AUTO_INCREMENT COMMENT '이미지 ID',
8686
`location` VARCHAR(80) NOT NULL COMMENT '이미지가 저장된 장소 (도메인 URL)',
8787
`created_at` timestamp DEFAULT NOW() COMMENT '생성시간',
8888
`updated_at` timestamp DEFAULT NOW() COMMENT '수정시간',
8989
`deleted_at` TIMESTAMP COMMENT '삭제시간 - NULL일 경우 삭제되지 않음'
9090
);
9191

9292
CREATE TABLE `resized_image` (
93-
`resized_image_id` bigint AUTO_INCREMENT COMMENT '리사이징 이미지 ID',
93+
`id` bigint AUTO_INCREMENT COMMENT '리사이징 이미지 ID',
9494
`image_id` bigint NOT NULL COMMENT '이미지 참조 FK',
9595
-- ENUM('THUMB','SMALL','LARGE','ETC') -> VARCHAR(10)
9696
`resized_image_url` varchar(255) NOT NULL COMMENT '리사이징 이미지 URL',
@@ -100,13 +100,13 @@ CREATE TABLE `resized_image` (
100100
);
101101

102102
CREATE TABLE `social_media_type` (
103-
`social_media_type_id` VARCHAR(20) PRIMARY KEY,
103+
`id` VARCHAR(20) PRIMARY KEY,
104104
`social_media_name` varchar(100) NOT NULL,
105105
`icon_id` BIGINT
106106
);
107107

108108
CREATE TABLE `social_media` (
109-
`social_media_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
109+
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
110110
`member_id` bigint NOT NULL,
111111
`social_media_type_id` VARCHAR(255) NOT NULL,
112112
`url` varchar(255) NOT NULL,
@@ -115,7 +115,7 @@ CREATE TABLE `social_media` (
115115
);
116116

117117
CREATE TABLE `tech_stack` (
118-
tech_stack_id BIGINT AUTO_INCREMENT PRIMARY KEY,
118+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
119119
code CHAR(3) NOT NULL UNIQUE,
120120
tech_stack_name VARCHAR(100) NOT NULL,
121121
parent_id BIGINT,
@@ -126,8 +126,8 @@ CREATE TABLE `tech_stack` (
126126
);
127127

128128
CREATE TABLE `tech_stack_ref` (
129-
`tech_stack_ref_id` bigint PRIMARY KEY AUTO_INCREMENT,
129+
`id` bigint PRIMARY KEY AUTO_INCREMENT,
130130
`tech_stack_id` BIGINT NOT NULL,
131131
`member_id` bigint NOT NULL,
132132
`type` VARCHAR(30) NOT NULL
133-
);
133+
);

src/main/java/com/codezerotoone/mvp/domain/common/BaseEntity.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ public abstract class BaseEntity {
2929
protected BaseEntity() {
3030
}
3131

32-
public LocalDateTime getCreatedAt() {
32+
// 변경 사유: 자신의 날짜는 여기저기서 꺼내 볼 수 있게 하는 것보다 스스로가 관리하는 것이 객체지향의 원칙에도, DDD의 지향점에도 부합한다고 생각됩니다.
33+
protected LocalDateTime getCreatedAt() {
3334
return createdAt;
3435
}
3536

36-
public LocalDateTime getUpdatedAt() {
37+
// 변경 사유: 자신의 날짜는 여기저기서 꺼내 볼 수 있게 하는 것보다 스스로가 관리하는 것이 객체지향의 원칙에도, DDD의 지향점에도 부합한다고 생각됩니다.
38+
protected LocalDateTime getUpdatedAt() {
3739
return updatedAt;
3840
}
3941

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.codezerotoone.mvp.domain.common;
2+
3+
import jakarta.persistence.*;
4+
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
5+
6+
import java.time.LocalDateTime;
7+
8+
import static com.codezerotoone.mvp.domain.common.EntityConstant.BOOLEAN_DEFAULT_FALSE;
9+
import static jakarta.persistence.GenerationType.IDENTITY;
10+
11+
// 추가 사유: 일반적으로 공통되는 컬럼들은 엔티티가 추가될 때마다 일일이 설정하기보다는 공용 멤버로 묶는 게 나아 보입니다.
12+
@MappedSuperclass
13+
@EntityListeners(AuditingEntityListener.class)
14+
public abstract class BaseGeneralEntity extends BaseEntity {
15+
@Id
16+
@GeneratedValue(strategy = IDENTITY)
17+
private Long id;
18+
19+
@Column(nullable = false, columnDefinition = BOOLEAN_DEFAULT_FALSE)
20+
private boolean deleteYn;
21+
22+
@Column(name = "deleted_at")
23+
private LocalDateTime deletedAt;
24+
25+
public Long getId() {
26+
return id;
27+
}
28+
29+
protected boolean isDeleted() {
30+
return deleteYn;
31+
}
32+
33+
protected void updateId(Long id) {
34+
this.id = id;
35+
}
36+
37+
protected void deleteEntity() {
38+
deleteYn = true;
39+
deletedAt = LocalDateTime.now();
40+
}
41+
42+
protected void undeleteEntity() {
43+
deleteYn = false;
44+
}
45+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.codezerotoone.mvp.domain.common;
2+
3+
public class EntityConstant {
4+
public static final String BOOLEAN_DEFAULT_FALSE = "boolean default false";
5+
}
Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.codezerotoone.mvp.domain.image.entity;
22

3-
import com.codezerotoone.mvp.domain.common.BaseEntity;
3+
import com.codezerotoone.mvp.domain.common.BaseGeneralEntity;
44
import com.codezerotoone.mvp.domain.image.entity.dto.ResizedImageInfo;
5-
import jakarta.persistence.*;
5+
import jakarta.persistence.CascadeType;
6+
import jakarta.persistence.Entity;
7+
import jakarta.persistence.OneToMany;
8+
import jakarta.persistence.Table;
69
import lombok.AccessLevel;
710
import lombok.Getter;
811
import lombok.NoArgsConstructor;
912

10-
import java.time.LocalDateTime;
1113
import java.util.ArrayList;
1214
import java.util.Arrays;
1315
import java.util.List;
@@ -16,18 +18,16 @@
1618
@Table(name = "image")
1719
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1820
@Getter
19-
public class Image extends BaseEntity {
20-
21-
@Id
22-
@GeneratedValue(strategy = GenerationType.IDENTITY)
23-
private Long imageId;
24-
25-
@OneToMany(mappedBy = "image", cascade = { CascadeType.PERSIST, CascadeType.REMOVE }) // TODO: 고아객체 삭제?
21+
public class Image extends BaseGeneralEntity {
22+
// 변경 사유: 원본 이미지와 연결되지 않은 경우 의미없는 데이터이므로, DB에 튜플을 남기지 않는 편이 장기적으로 낫다고 보입니다.
23+
@OneToMany(mappedBy = "image", cascade = {CascadeType.PERSIST}, orphanRemoval = true)
2624
private List<ResizedImage> resizedImages = new ArrayList<>();
2725

2826
private String location;
2927

30-
private LocalDateTime deletedAt = null;
28+
private Image(Long id) {
29+
updateId(id);
30+
}
3131

3232
public static Image create(String location, ResizedImageInfo... resizedImages) {
3333
Image image = new Image();
@@ -47,17 +47,17 @@ public static Image create(String location, List<ResizedImageInfo> resizedImages
4747
return image;
4848
}
4949

50-
public static Image getReference(Long id) {
51-
Image image = new Image();
52-
image.imageId = id;
53-
return image;
50+
// 변경 사유: 메서드명과 역할 불일치
51+
public static Image of(Long id) {
52+
// 변경 사유: 생성 로직 간소화
53+
return new Image(id);
5454
}
5555

5656
public void delete() {
57-
this.deletedAt = LocalDateTime.now();
57+
deleteEntity();
5858
}
5959

6060
public boolean isDeleted() {
61-
return this.deletedAt != null;
61+
return isDeleted();
6262
}
6363
}

src/main/java/com/codezerotoone/mvp/domain/image/entity/ResizedImage.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88

99
import java.time.LocalDateTime;
1010

11+
import static com.codezerotoone.mvp.domain.common.EntityConstant.BOOLEAN_DEFAULT_FALSE;
12+
1113
@Entity
1214
@Table(name = "resized_image")
1315
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1416
@Getter
1517
public class ResizedImage {
16-
1718
@Id
1819
@GeneratedValue(strategy = GenerationType.IDENTITY)
19-
private Long resizedImageId;
20+
private Long id;
2021

2122
@ManyToOne(fetch = FetchType.LAZY)
2223
@JoinColumn(name = "image_id")
@@ -28,7 +29,11 @@ public class ResizedImage {
2829
@Column(name = "image_size_type", columnDefinition = "VARCHAR(10)")
2930
private ImageSizeType imageSizeType;
3031

31-
public LocalDateTime deletedAt = null;
32+
@Column(nullable = false, columnDefinition = BOOLEAN_DEFAULT_FALSE)
33+
private boolean deleteYn;
34+
35+
@Column(name = "deleted_at")
36+
private LocalDateTime deletedAt;
3237

3338
public ResizedImage(Image image, String resizedImageUrl, ImageSizeType imageSizeType) {
3439
this.image = image;
@@ -37,15 +42,12 @@ public ResizedImage(Image image, String resizedImageUrl, ImageSizeType imageSize
3742
}
3843

3944
public void delete() {
40-
this.deletedAt = LocalDateTime.now();
45+
deleteYn = true;
46+
deletedAt = LocalDateTime.now();
4147
}
4248

4349
public boolean isDeleted() {
44-
return this.deletedAt != null;
45-
}
46-
47-
public boolean isNotDeleted() {
48-
return !isDeleted();
50+
return deleteYn;
4951
}
5052

5153
public String getFullResizedImageUrl() {

src/main/java/com/codezerotoone/mvp/domain/image/repository/ImageRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ public interface ImageRepository extends JpaRepository<Image, Long> {
1313
@Query("""
1414
SELECT i
1515
FROM Image i
16-
WHERE i.deletedAt IS NULL
17-
AND i.imageId = :imageId
16+
WHERE i.deleteYn = false
17+
AND i.id = :imageId
1818
""")
1919
Optional<Image> findById(@Param("imageId") Long imageId);
2020
}

src/main/java/com/codezerotoone/mvp/domain/image/service/ImageService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ public class ImageService {
2424
public Long saveImage(String location, List<ResizedImageInfo> imageInfos) {
2525
Image newImage = Image.create(location, imageInfos);
2626
newImage = this.imageRepository.save(newImage);
27-
return newImage.getImageId();
27+
return newImage.getId();
2828
}
2929
}

0 commit comments

Comments
 (0)