포스트

원티드 프리온보딩 백엔드 API 서버 요구사항 분석

주어진 채용공고 API 응답 예시로 요구사항 분석하기

채용공고 생성

주어진 응답예시는 다음과 같습니다.

1
2
3
4
5
6
7
{  
"회사_id": 회사_id,  
"채용포지션": "백엔드 주니어 개발자",  
"채용보상금": 1000000,  
"채용내용": "원티드랩에서 백엔드 주니어 개발자를 채용합니다. 자격요건은..",  
"사용기술": "Python"  
}

예상 필드값

채용공고 생성 API 응답을 통해서, 채용공고의 필드값을 유추할 수 있습니다.

필드데이터 타입Nullable고려 사항
채용포지션Stringx 
채용보상금Numberx채용보상금은 절대로 음수가 될 수 없음
채용내용Stringx채용 내용은 큰 공간을 필요로 함 (HTML 등이 포함될 수 있음)
사용기술Stringx채용공고당 사용기술이 하나만 오진 않을 것이나,
API 응답은 Array가 아닌 String임으로 무시하도록 함

예상 Endpoint

API Endpoint는 다음처럼 하면 될 것 같습니다.

1
POST /api/v1/job-posting

채용공고 목록 조회

주어진 응답예시는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[  
{   
  "채용공고_id": 채용공고_id,  
  "회사명": "원티드랩",  
  "국가": "한국",  
  "지역": "서울",  
  "채용포지션": "백엔드 주니어 개발자",  
  "채용보상금": 1500000,  
  "사용기술": "Python"  
},  {    "채용공고_id": 채용공고_id,  
  "회사명": "네이버",  
  "국가": "한국",  
  "지역": "판교",  
  "채용포지션": "Django 백엔드 개발자",  
  "채용보상금": 1000000,  
  "사용기술": "Django"  
},  ...]

예상 필드값

채용공고 목록 조회를 통해서 기업의 필드값을 유추할 수 있었습니다.

필드데이터 타입Nullable
회사명Stringx
국가Stringx
지역Stringx

예상 Endpoint

API Endpoint는 다음처럼 하면 될 것 같습니다.

1
GET /api/v1/job-posting

채용공고 수정

예상 주의 포인트

  • 자신이 올린 채용공고에 대해서만 수정이 가능해야합니다.
  • 부분 수정이 가능해야합니다.

예상 Endpoint

API Endpoint는 다음처럼 하면 될 것 같습니다.

1
PATCH /api/v1/job-posting/{jobPostingId}

채용공고 삭제

예상 주의 포인트

  • 자신이 올린 채용공고에 대해서만 삭제가 가능해야합니다.

예상 Endpoint

API Endpoint는 다음처럼 하면 될 것 같습니다.

1
DELETE /api/v1/job-posting/{jobPostingId}

데이터베이스 설계

채용공고

MySQL를 사용할터이니 PK 필드를 추가합니다.

추가됨필드데이터 타입Nullable
(+)PKNumberx
 채용포지션Stringx
 채용보상금Numberx
 채용내용Stringx
 사용기술Stringx

계정의 생성일, 수정일, 삭제일도 확인 해야하므로 Auditing 필드를 추가하겠습니다.

추가됨필드데이터 타입Nullable
 PKNumberx
 채용포지션Stringx
 채용보상금Numberx
 채용내용Stringx
 사용기술Stringx
(+)채용 공고 생성일Datetimex
(+)채용 공고 수정일Datetimex
(+)채용 공고 삭제일Datetime 

MySQL 테이블 생성

위에서 생각한대로 실제 테이블을 만들어보겠습니다.

  • job_posting table

    필드데이터 타입기본값설명
    idxbigint-AUTO_INCREMENT
    company_idbigint-기업 FK
    recruit_rewardint0채용 보상금 not null
    recruit_positionvarchar(30)-채용 포지션 not null
    descriptiontext-채용 내용 not null
    required_skillvarchar(20)-요구 기술 not null
    created_atdatetime-생성 시간 not null
    updated_atdatetime-마지막 수정 시간 not null
    deleted_atdatetimenull삭제 시간
    제약 조건설명
    PRIMARY KEY (idx) 
    KEY job_posting_idx_index 
    KEY job_posting_user_idx_fk 
    FOREIGN KEY (company_id)REFERENCES user (idx)
  • DDL

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    CREATE TABLE `job_posting` (
    `idx` bigint NOT NULL AUTO_INCREMENT, 
    `company_id` bigint COMMENT '기업 FK', 
    `recruit_reward` int NOT NULL DEFAULT '0' COMMENT '채용 보상금', 
    `recruit_position` varchar(30) NOT NULL COMMENT '채용 포지션', 
    `description` text NOT NULL COMMENT '채용 내용', 
    `required_skill` varchar(20) NOT NULL COMMENT '요구 기술', 
    `created_at` datetime not null, 
    `updated_at` datetime not null, 
    `deleted_at` datetime null,
    PRIMARY KEY (`idx`), 
    KEY `job_posting_idx_index` (`idx`), 
    KEY `job_posting_user_idx_fk` (`company_id`), 
    CONSTRAINT `job_posting_user_idx_fk` FOREIGN KEY (`company_id`) REFERENCES `user` (`idx`)
    ) ENGINE = InnoDB AUTO_INCREMENT = 30006 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '채용 공고'
    

Entity Class 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Getter  
@Entity  
@Table(name = "job_posting", schema = "wanted2023")  
@SQLDelete(sql = "UPDATE user SET deleted_at = NOW() WHERE idx = ?")  
@Where(clause = "deleted_at IS NULL")  
@EntityListeners(AuditingEntityListener.class)  
public class JobPosting {  
  
  @GeneratedValue(strategy = GenerationType.IDENTITY)  
  @Id  
  @Column(name = "idx")  
  private Long id;  
  
  @OneToOne  
  @JoinColumn(name = "company_id")  
  private EnterpriseUserAccount company;  
  
  @Column(name = "recruit_reward")  
  private int recruitReward;  
  
  @Column(name = "recruit_position")  
  private String recruitPosition;  
  
  @Column(name = "description")  
  private String description;  
  
  @Column(name = "required_skill")  
  private String requiredSkill;  
  
  @CreatedDate  
  @Column(name = "created_at")  
  private LocalDateTime createdAt;  
  
  @LastModifiedDate  
  @Column(name = "updated_at")  
  private LocalDateTime updatedAt;  
  
  @Column(name = "deleted_at")  
  private LocalDateTime deletedAt;  
  
  @Builder  
  public JobPosting(int recruitReward, String recruitPosition, String description,  
      String requiredSkill) {  
    this.recruitReward = recruitReward;  
    this.recruitPosition = recruitPosition;  
    this.description = description;  
    this.requiredSkill = requiredSkill;  
  }  
  
  public JobPosting() {  
  
  }  
  
  @Override  
  public boolean equals(Object o) {  
    if (this == o) {  
      return true;  
    }  
    if (o == null || getClass() != o.getClass()) {  
      return false;  
    }  
  
    JobPosting that = (JobPosting) o;  
  
    return Objects.equals(id, that.id);  
  }  
  
  @Override  
  public int hashCode() {  
    int result = (int) (id ^ (id >>> 32));  
    result = 31 * result + (company != null ? company.hashCode() : 0);  
    result = 31 * result + recruitReward;  
    result = 31 * result + (recruitPosition != null ? recruitPosition.hashCode() : 0);  
    result = 31 * result + (description != null ? description.hashCode() : 0);  
    return result;  
  }  
  
  public void setCompany(EnterpriseUserAccount company) {  
    this.company = company;  
  }
  
}

JobPosting.class에서 짚을 수 있는 포인트들은 다음과 같습니다.

  1. JPA Auditing 적용
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    ....
    @EntityListeners(AuditingEntityListener.class)  
    public class JobPosting {
     ....
    	
     @LastModifiedDate  
     @Column(name = "updated_at")  
     private LocalDateTime updatedAt;  
    	
     @Column(name = "deleted_at")  
     private LocalDateTime deletedAt;
    	
     ....
    }
    
  2. Soft Delete 적용
    관련 포스트 : MySQL 물리삭제와 논리삭제
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    ....
    @SQLDelete(sql = "UPDATE user SET deleted_at = NOW() WHERE idx = ?")  
    @Where(clause = "deleted_at IS NULL")
    public class JobPosting {
     ....
    	
     @Column(name = "deleted_at")  
     private LocalDateTime deletedAt;
    	
     ....
    }
    
    • @SQLDelete() : 삭제할때 Deleted_at을 현재 시간으로 설정하여 SoftDelete를 구현할 수 있습니다.
    • @Where() : 조회할때 delete_at 필드가 Null인 Row만 가져오도록 합니다.
  3. OneToOne 연관관계 맵핑
    1
    2
    3
    4
    5
    6
    7
    
     @OneToOne  
     @JoinColumn(name = "company_id")  
     private EnterpriseUserAccount company;  
    
     public void setCompany(EnterpriseUserAccount company) {  
         this.company = company;  
     }
    
    • @OneToOne, @JoinColumn() : 해당 어노테이션으로 참조 관계임을 명시해줍니다.
    • setCompany() : 채용공고 생성시 setCompany()를 통해서 참조할 기업 객체를 지정해줄 수 있습니다.

기업(채용공고를 올리는 주체)

Mysql를 사용할터이니 PK 필드를 추가합니다.

추가됨필드데이터 타입Nullable
(+)PKNumberx
 회사명Stringx
 국가Stringx
 지역Stringx

여기서 로그인을 위한 ID, PW 필드도 추가해주었습니다.

추가됨필드데이터 타입Nullable
 PKNumberx
(+)아이디Stringx
(+)비밀번호Stringx
 회사명Stringx
 국가Stringx
 지역Stringx

계정의 생성일, 수정일, 삭제일도 확인 해야하므로 Auditing 필드를 추가하겠습니다.

추가됨필드데이터 타입Nullable
 PKNumberx
 아이디Stringx
 비밀번호Stringx
 회사명Stringx
 국가Stringx
 지역Stringx
(+)계정 생성일Datetimex
(+)계정 수정일Datetimex
(+)계정 삭제일Datetime 

근데, 기업만 로그인하는 것이아닌, 채용공고에 지원하려는 일반 유저도 있을거기때문에 그에 해당할 필드를 추가해주겠습니다. SingleTable 전략을 사용할거기 때문에 Nullable 칼럼은 지워주도록 하겠습니다.

추가됨필드데이터 타입
 PKNumber
 아이디String
 비밀번호String
(+)사용자 실명String
(+)사용자 생년월일Date
 회사명String
 국가String
 지역String
 계정 생성일Datetime
 계정 수정일Datetime
 계정 삭제일Datetime

이렇게 보면, 다소 혼란스러우니 SingleTable의 DiscriminatorColumn을 설정하여 일반유저와 기업유저를 분리하겠습니다.

필드데이터 타입Discrimator
PKNumber-
아이디String-
비밀번호String-
사용자 실명String일반 유저
사용자 생년월일Date일반 유저
회사명String기업 유저
국가String기업 유저
지역String기업 유저
계정 생성일Datetime-
계정 수정일Datetime-
계정 삭제일Datetime-

MySQL 테이블 생성

지금까지 정리해둔 정보로 실제 MySQL의 테이블을 생성해보겠습니다.

  • user table

    필드데이터 타입기본값설명
    idxbigint-AUTO_INCREMENT, PRIMARY KEY
    usernamevarchar(50)-not null
    passwordvarchar(100)-not null
    rolevarchar(15)‘personal’not null
    is_bantinyint(1)0not null
    personal_namevarchar(30)-실명 (null 허용)
    personal_brithdate-개인 - 생년월일 (null 허용)
    enterprise_namevarchar(20)-기업명 (null 허용)
    enterprise_nation_codevarchar(5)-기업 - 국가 코드 (null 허용)
    enterprise_province_codevarchar(10)-기업 - 지역 코드 (null 허용)
    created_atdatetime-생성 시간 not null
    updated_atdatetime-마지막 수정 시간 not null
    deleted_atdatetimenull삭제 시간
    인덱스필드
    user_idx_indexidx
  • DDL

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    create table wanted2023.user (
    idx bigint auto_increment primary key, 
    username varchar(50) not null, 
    password varchar(100) not null, 
    role varchar(15) default 'personal' not null, 
    is_ban tinyint(1) default 0 not null, 
    personal_name varchar(30) null comment '실명', 
    personal_brith date null comment '개인 - 생년월일', 
    enterprise_name varchar(20) null, 
    enterprise_nation_code varchar(5) null, 
    enterprise_province_code varchar(10) null, 
    created_at datetime not null, 
    updated_at datetime not null, 
    deleted_at datetime null
    );
    create index user_idx_index on wanted2023.user (idx);
    

Entity Class 생성

DDL을 토대로 Entity Class를 만들어보도록 하겠습니다.

UserAccount.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Getter  
@Entity(name = "user")  
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)  
@DiscriminatorColumn(name = "role")  
@SQLDelete(sql = "UPDATE user SET deleted_at = NOW() WHERE idx = ?")  
@Where(clause = "deleted_at IS NULL")  
@EntityListeners(AuditingEntityListener.class)  
public abstract class UserAccount {  
  
  @GeneratedValue(strategy = GenerationType.IDENTITY)  
  @Id  
  @Column(name = "idx")  
  private Long id;  
  
  @Column(name = "username")  
  private String username;  
  
  @Column(name = "password")  
  private String password;  
  
  @Builder.Default  
  @Column(name = "is_ban")  
  private boolean isBan = false;  
  
  @CreatedDate  
  @Column(name = "created_at")  
  private LocalDateTime createdAt;  
  
  @LastModifiedDate  
  @Column(name = "updated_at")  
  private LocalDateTime updatedAt;  
  
  @Column(name = "deleted_at")  
  private LocalDateTime deletedAt;  
  
  protected UserAccount(String username, String password, boolean isBan) {  
    this.username = username;  
    this.password = password;  
    this.isBan = isBan;  
  }  
  
  protected UserAccount() {  
  
  }  
  
  @Override  
  public boolean equals(Object o) {  
    if (this == o) {  
      return true;  
    }  
    if (o == null || getClass() != o.getClass()) {  
      return false;  
    }  
  
    UserAccount user = (UserAccount) o;  
  
    return Objects.equals(id, user.id);  
  }  
  
}

UserAccount.class에서 짚을 수 있는 포인트들은 다음과 같습니다.

  1. Abstract Class 적용
    일반 유저 및 기업 유저에서 추상 클래스를 공통 필드를 상속받도록 하고자 이를 추상클래스로 만들었습니다.
    UserAccount.class 단독 인스턴스 생성을 막기 위함이기도 합니다.

  2. 싱글테이블 전략 적용
    1
    2
    
    @Inheritance(strategy = InheritanceType.SINGLE_TABLE)  
    @DiscriminatorColumn(name = "role")
    

    SpringSecurity의 User 객체의 권한으로 사용될 role 칼럼값으로 일반, 기업 유저를 분류할 수 있습니다.(꿩먹고 알먹기)

  3. JPA Auditing 적용
    데이터베이스 설계 > 채용공고 > EntityClass 생성 항목을 참고해주세요.

  4. Soft Delete 적용
    데이터베이스 설계 > 채용공고 > EntityClass 생성 항목을 참고해주세요.

EnterpriseUserAccount.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Entity  
@Getter  
@NoArgsConstructor  
@DiscriminatorValue("ENTERPRISE")  
public class EnterpriseUserAccount extends UserAccount {  
  
  @Column(name = "enterprise_name")  
  private String name;  
  
  @Column(name = "enterprise_nation_code")  
  private String nationCode;  
  
  @Column(name = "enterprise_province_code")  
  private String provinceCode;  
  
  @Builder  
  public EnterpriseUserAccount(String username, String password, boolean isBan, String name,  
      String nationCode, String provinceCode) {  
    super(username, password, isBan);  
    this.name = name;  
    this.nationCode = nationCode;  
    this.provinceCode = provinceCode;  
  }  
  
}

PersonalUserAccount.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity  
@NoArgsConstructor  
@DiscriminatorValue("PERSONAL")  
public class PersonalUserAccount extends UserAccount {  
  
  @Column(name = "personal_name")  
  private String name;  
  
  @Column(name = "personal_brith")  
  private LocalDate birth;  
  
  @Builder  
  public PersonalUserAccount(String username, String password, boolean isBan, String name,  
      LocalDate birth) {  
    super(username, password, isBan);  
    this.name = name;  
    this.birth = birth;  
  }  
}

이제 다음 포스트에서 API를 직접 구현해보도록 하겠습니다. 👋👋

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

Comments powered by Disqus.