원티드 프리온보딩 백엔드 API 서버 요구사항 분석
주어진 채용공고 API 응답 예시로 요구사항 분석하기
채용공고 생성
주어진 응답예시는 다음과 같습니다.
1
2
3
4
5
6
7
{
"회사_id": 회사_id,
"채용포지션": "백엔드 주니어 개발자",
"채용보상금": 1000000,
"채용내용": "원티드랩에서 백엔드 주니어 개발자를 채용합니다. 자격요건은..",
"사용기술": "Python"
}
예상 필드값
채용공고 생성 API 응답을 통해서, 채용공고의 필드값을 유추할 수 있습니다.
필드 | 데이터 타입 | Nullable | 고려 사항 |
---|---|---|---|
채용포지션 | String | x | |
채용보상금 | Number | x | 채용보상금은 절대로 음수가 될 수 없음 |
채용내용 | String | x | 채용 내용은 큰 공간을 필요로 함 (HTML 등이 포함될 수 있음) |
사용기술 | String | x | 채용공고당 사용기술이 하나만 오진 않을 것이나, 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 |
---|---|---|
회사명 | String | x |
국가 | String | x |
지역 | String | x |
예상 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 |
---|---|---|---|
(+) | PK | Number | x |
채용포지션 | String | x | |
채용보상금 | Number | x | |
채용내용 | String | x | |
사용기술 | String | x |
계정의 생성일, 수정일, 삭제일도 확인 해야하므로 Auditing
필드를 추가하겠습니다.
추가됨 | 필드 | 데이터 타입 | Nullable |
---|---|---|---|
PK | Number | x | |
채용포지션 | String | x | |
채용보상금 | Number | x | |
채용내용 | String | x | |
사용기술 | String | x | |
(+) | 채용 공고 생성일 | Datetime | x |
(+) | 채용 공고 수정일 | Datetime | x |
(+) | 채용 공고 삭제일 | Datetime |
MySQL 테이블 생성
위에서 생각한대로 실제 테이블을 만들어보겠습니다.
job_posting table
필드 데이터 타입 기본값 설명 idx bigint - AUTO_INCREMENT company_id bigint - 기업 FK recruit_reward int 0 채용 보상금 not null recruit_position varchar(30) - 채용 포지션 not null description text - 채용 내용 not null required_skill varchar(20) - 요구 기술 not null created_at datetime - 생성 시간 not null updated_at datetime - 마지막 수정 시간 not null deleted_at datetime null 삭제 시간 제약 조건 설명 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
에서 짚을 수 있는 포인트들은 다음과 같습니다.
- 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; .... }
- 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만 가져오도록 합니다.
- 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 |
---|---|---|---|
(+) | PK | Number | x |
회사명 | String | x | |
국가 | String | x | |
지역 | String | x |
여기서 로그인을 위한 ID, PW 필드도 추가해주었습니다.
추가됨 | 필드 | 데이터 타입 | Nullable |
---|---|---|---|
PK | Number | x | |
(+) | 아이디 | String | x |
(+) | 비밀번호 | String | x |
회사명 | String | x | |
국가 | String | x | |
지역 | String | x |
계정의 생성일, 수정일, 삭제일도 확인 해야하므로 Auditing
필드를 추가하겠습니다.
추가됨 | 필드 | 데이터 타입 | Nullable |
---|---|---|---|
PK | Number | x | |
아이디 | String | x | |
비밀번호 | String | x | |
회사명 | String | x | |
국가 | String | x | |
지역 | String | x | |
(+) | 계정 생성일 | Datetime | x |
(+) | 계정 수정일 | Datetime | x |
(+) | 계정 삭제일 | Datetime |
근데, 기업만 로그인하는 것이아닌, 채용공고에 지원하려는 일반 유저도 있을거기때문에 그에 해당할 필드를 추가해주겠습니다. SingleTable 전략을 사용할거기 때문에 Nullable 칼럼은 지워주도록 하겠습니다.
추가됨 | 필드 | 데이터 타입 |
---|---|---|
PK | Number | |
아이디 | String | |
비밀번호 | String | |
(+) | 사용자 실명 | String |
(+) | 사용자 생년월일 | Date |
회사명 | String | |
국가 | String | |
지역 | String | |
계정 생성일 | Datetime | |
계정 수정일 | Datetime | |
계정 삭제일 | Datetime |
이렇게 보면, 다소 혼란스러우니 SingleTable의 DiscriminatorColumn
을 설정하여 일반유저와 기업유저를 분리하겠습니다.
필드 | 데이터 타입 | Discrimator |
---|---|---|
PK | Number | - |
아이디 | String | - |
비밀번호 | String | - |
사용자 실명 | String | 일반 유저 |
사용자 생년월일 | Date | 일반 유저 |
회사명 | String | 기업 유저 |
국가 | String | 기업 유저 |
지역 | String | 기업 유저 |
계정 생성일 | Datetime | - |
계정 수정일 | Datetime | - |
계정 삭제일 | Datetime | - |
MySQL 테이블 생성
지금까지 정리해둔 정보로 실제 MySQL의 테이블을 생성해보겠습니다.
user table
필드 데이터 타입 기본값 설명 idx bigint - AUTO_INCREMENT, PRIMARY KEY username varchar(50) - not null password varchar(100) - not null role varchar(15) ‘personal’ not null is_ban tinyint(1) 0 not null personal_name varchar(30) - 실명 (null 허용) personal_brith date - 개인 - 생년월일 (null 허용) 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 삭제 시간 인덱스 필드 user_idx_index idx 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
에서 짚을 수 있는 포인트들은 다음과 같습니다.
Abstract Class 적용
일반 유저 및 기업 유저에서 추상 클래스를 공통 필드를 상속받도록 하고자 이를 추상클래스로 만들었습니다.
UserAccount.class
단독 인스턴스 생성을 막기 위함이기도 합니다.- 싱글테이블 전략 적용
1 2
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "role")
SpringSecurity의 User 객체의 권한으로 사용될
role
칼럼값으로 일반, 기업 유저를 분류할 수 있습니다.(꿩먹고 알먹기) JPA Auditing 적용
데이터베이스 설계
>채용공고
>EntityClass 생성
항목을 참고해주세요.- 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를 직접 구현해보도록 하겠습니다. 👋👋
Comments powered by Disqus.