2024. 5. 14. 11:58ㆍ좋은 코드 (Good Code)/좋은 설계 (Good Design)
Drawing
나름 Swagger 결과 데이터를 보면서 만든 인터페이스를 소개하자면
interface SwaggerPath {
path: string;
method: string;
operationId: string;
parameters?: SwaggerPathParameter[];
requests?: SwaggerPathRequest[];
responses: SwaggerPathResponse[];
tags: string[];
}
interface SwaggerPathParameter {
name: string;
required: boolean;
type: string;
}
interface SwaggerPathRequest {
contentType: string;
schema?: string;
required: boolean;
}
interface SwaggerPathResponse {
statusCode: number;
description?: string;
content?: SwaggerPathResponseContent;
}
와 같다.
좋은 설계는 실제 현상을 기반으로 만든다. 원칙을 적용한 것이다.
다음은 좋은 설계는 목적을 충실히 반영해야 한다. 원칙을 지키기위해 목적을 먼저 생각해봤다.
Path를 통해 인증 & 인가를 할 수 있어야 한다.
이를 위해 좋은 설계는 근본을 찾는 것부터 시작한다. 원칙을 지키기 위해 필요한 것만 나열해봤다.
- Path는 어디로부터 왔는 지 알기 위해 서버 출처가 필요하다.
- Path는 서버 + 메소드 + 주소 조합을 키로 가진다.
- Path는 인증 여부를 확인하기 위한 필드를 가져야 한다. (open api 인지 확인)
- Path에는 인가 여부를 확인하기 위해 역할들을 저장해야 한다.
- Path에는 버전을 적용해서 업데이트 여부를 확인할 수 있도록 한다.
자, 이제 Path가 가져야 할 게 보였다.
- 서버
- 메소드
- 주소
- 인증 여부
- 역할들
- 버전
operationId, tags가 사라진 깔끔한 모습이 완성되었다.
좋은 설계는 변경에 닫혀있고 확장에는 열려있어야 한다. 원칙을 위해 서버를 외부 테이블로 뒀다.
역할(role)의 위치가 살짝 애매했는데, role의 속성이 변경되었다고 가정을 해보면... 여전히 사용자의 역할과 api의 역할을 비교할 때 키 값만 사용할 것이라, 변경에 닫혀있기 때문에 함께 뒀다.
이제 좋은 설계는 쉽고 정확하게 코드를 작성하게 해준다. 원칙을 증명할 차례다.
Behavior
우선 테이블부터 생성해보자
CREATE TABLE ploy.api_paths
(
srl bigint NOT NULL COMMENT 'primary key of table' AUTO_INCREMENT,
serverSrl bigint NOT NULL COMMENT 'server srl (api_servers)',
method varchar(16) NOT NULL COMMENT 'method',
path varchar(128) NOT NULL COMMENT 'path',
opened boolean NOT NULL COMMENT 'is it opened for anyone',
roleLabels json NOT NULL COMMENT 'role labels for authentication',
version bigint NOT NULL COMMENT 'version',
createdBy varchar(32) NOT NULL COMMENT 'who created this',
createdAt datetime(6) NOT NULL COMMENT 'creation datetime',
modifiedBy varchar(32) NOT NULL COMMENT 'who modified this',
modifiedAt datetime(6) NOT NULL COMMENT 'modification datetime',
PRIMARY KEY (srl),
UNIQUE KEY uk1 (serverSrl, method, path)
) COMMENT ''
;
이를 통해 엔티티 클래스도 만들어주고,
@Entity
@Table(name = "api_paths")
class ApiPath private constructor(
@ManyToOne
@JoinColumn(name = "serverSrl")
var server: ApiServer,
@Enumerated(value = EnumType.STRING)
var method: ApiMethod,
var path: String,
var opened: Boolean = false,
@Type(value = JsonType::class)
@Column(columnDefinition = "json")
var roleLabels: MutableList<String> = mutableListOf(),
@Version
var version: Long = 0,
createdBy: String
) :
BaseEntity(createdBy = createdBy)
다시 미들 서버로 넘어와 모델을 정의하고
interface Base {
srl: bigint;
createdBy: string;
createdAt: string;
modifiedBy: string;
modifiedAt: string;
}
type ApiMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
export interface ApiPath extends Base {
serverSrl: number;
method: ApiMethod;
path: string;
opened: boolean;
roleLabels: string[];
version: number;
}
한 가지 예시로 인가 여부를 확인하는 코드도 작성해주고
export function isAuthorized(
userRoleLabels: string[],
apiRoleLabels: string[]
): boolean {
if (userRoleLabels.length === 0) {
return false;
}
if (apiRoleLabels.length === 0) {
return false;
}
return apiRoleLabels.some((apiRoleLabel) =>
userRoleLabels.includes(apiRoleLabel)
);
}
테스트 코드까지 통과하면 완성이다!
Comment
로버트 C. 마틴의 Clean Architecture 책을 읽었을 때 감명 깊게 느낀 문구가 하나 있었다.
빨리 가는 유일한 방법은 제대로 가는 것이다.
필자는 일단 해보고 고민하는 성향이라 우선 코드부터 작성했는데,
어색하고 불필요한 것들이 너무 많았으며 - 심지어 만들면 안되는 엔티티까지 끄적였다.
이게 뭔데...
@Entity
@Table(name = "api_path_descriptions")
class ApiPathDescription(
@ManyToOne(fetch = FetchType.LAZY, optional = false)
var path: ApiPath,
var functionName: String,
@Type(value = JsonType::class)
@Column(columnDefinition = "json")
var parameterMap: Map<String, String>,
@Type(value = JsonType::class)
@Column(columnDefinition = "json")
var bodyMap: Map<String, String>,
@Type(value = JsonType::class)
@Column(columnDefinition = "json")
var responseMap: Map<String, String>,
createdBy: String
) :
BaseEntity(createdBy = createdBy)
어떻게 해야하나... 답이 없었기 때문에, 또 하나의 성격인 계획형이 발휘되었다.
그래, 실무에서는 남이 시키든 안 시키든 다이어그램부터 그리지 않았나.
좋은 설계를 시작하면서 글에서 서술했듯 하나씩 차근차근 원칙을 세워가며 해보니, 1시간도 안되서 깔끔하게 개발했다.
그동안 헛짓거리했던 코드를 지우면서 다시 느낀건데,
버렸던 내 시간을 회수하기 위해 좋은 설계란 무엇인지 계속 탐구해야 된다는 점과
좋은 코드는 어떻게 작성하느냐부터 출발하는 것이 아닌, 어떻게 설계하는 가부터 시작해야되는 것이다.
'좋은 코드 (Good Code) > 좋은 설계 (Good Design)' 카테고리의 다른 글
인증 & 인가 서비스를 위한 Swagger 기반의 ERD 그리기 2 (0) | 2024.05.14 |
---|---|
좋은 설계를 시작하면서 (0) | 2024.05.14 |