Post

클린코드와 SOLID 원칙: 변경에 유연한 객체지향 설계 방법

프리온보딩 백엔드 챌린지 변경에 유연한 코드 설계 : 클린코드와 SOLID까지 강의를 정리한 내용입니다.

의미있는 이름

1.의도를 분명히 밝혀라

변수 / 함수명만 봐도 어떤 동작을 해야하는지 예측이 되어야 합니다. 즉, 이름에서 의도를 분명히 밝혀야 합니다. 만약, 의미가 명확하지 않으면 이름이 길어지더라도 이름에 의미를 붙여 작성할 필요가 있습니다.

2.그릇된 정보를 피하라

코드에 코드 의미를 흐릴 수 있는 그릇된 단서를 남겨선 안됩니다.

  • 예로 숫자 1과 소문자 L 구분 / 숫자 0과 대문자 O 구분은 어렵습니다.
  • 리스트로 되어있더라도 변수명에 list를 포함하지 않는 편이 좋습니다.

3.의미있게 구분하라

연속된 숫자를 덧붙이거나 noise word를 추가하는 방식은 적절하지 못합니다.

  • new CustomerObject(“이름”); -> 굳이 Object를 붙이지 않아도 클래스명을 대문자로 시작하기 때문에 Object라는 단어는 불필요 합니다. 단, 꼭 Type을 써야하는 이유가 명확하고 반박할 수 없는 근거가 있다면 명시해도 괜챃습니다.
  • getActiveAccount(), getActiveAccounts(), getActiveAccountInfo() -> 이름을 작성한 사람의 의도는 Info가 붙은게 더 자세히 보일 것입니다.

4.발음하기 쉬운 이름을 사용하라

  • genymdhms(generate date, year,month, day,hour,minute,second)와 같은 이름을 피해야합니다. generationTimeStamp, modificationTimeStamp, recordId 등 의미와 발음하기 쉽도록 작성해야합니다.

5.검색하기 쉬운 이름을 사용하라

문자 하나를 사용하는 이름과 상수는 텍스트 코드에서 쉽게 눈에 띄지 않습니다.

  • e라는 문자는 변수 이름으로 적합하지 못합니다. 검색하기 쉬운 이름을 사용하면 로그를 찾을 때도 더 잘 필터링이 될 것입니다.

6. 타입과 관련된 문자열을 넣지 말아라

타입이 바뀔 수 있기 때문입니다.

단, 예외사항이 있습니다.

인터페이스 클래스와 구현 클래스의 경우, 인터페이스 클래스는 접두어를 붙이지 않고, 구체 클래스에 접두어를 붙이는 것은 가독성이 좋기 때문에 타입과 관련된 문자열을 넣습니다.

  • 인터페이스명 : ShapeFactorty
  • 구현체 : ShapeFactoryImp

7.한 개념에 한 단어를 사용하라

일관성 있는 어휘를 선택해서 이름을 붙여야합니다. 단, 과거에는 서비스 계층 별로 어휘를 나눠서 통일화 시키는 케이스가 있습니다.

  • Controller 계층에서 fetch, service 계층에서는 get을 사용

8.의미 있는 맥락을 추가하라

String firstName, lastName 보다 맥락을 좀 더 분명하게 하기 위해서 addr라는 접두어를 붙이면 String addrFirstName, String addrLastName라고 좀 더 명확하게 의미를 알 수 있습니다.

9.불필요한 맥락을 없애라

의미없는 접수를 붙이지 않고, 의미 전달이 제대로 안되는 짧은 변수명은 좋지 않은 이름입니다.


Function

1.작게 만들어라

한가지만 하도록하고, 작게(적은 줄) 만드는 것이 좋습니다. -> Tip : 요구사항대로 테스트코드를 작성하고 테스트 코드를 토대로 분리될 수 있는 부분을 함수로 분리하면 단위를 좀 더 작게 가져갈 수 있습니다. 또한, 함수는 위에서 아래로 이야기 처럼 읽혀야 좋습니다.

한 함수 다음에는 추상화 수준이 한 단계씩 낮아져야 합니다. image

2.함수의 인수 종류와 개수

인수의 개수는 최대한 없는 것이 좋습니다. 불가피한 경우 1~2개정도가 적당합니다. 인수의 종류는 Input(입력 값)으로 인식합니다. 따라서 Output 역할을 하는 변수를 전달하는 것이 좋지 않습니다.

3.명령과 조회를 구분해라

객체 상태를 변경하는 행동을하거나 객체 정보를 반환하거나 둘 중 하나만 해야합니다. 만약 둘다 한다면 잘못된 함수 일 수 있습니다.

4. 오류 코드보다 예외를 사용하라

Emum 클래스로 코드를 정의해서 분기처리 하는 것보다 Try-catch문을 이용해서 예외를 처리하는 것이 코드의 가독성을 높여주고 변경의 유연성을 가질 수 있습니다.

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
//bad
if(deleteMember(memberDto) == E_OK){
	if(userRespository.reRegister(memberDto) == E_OK){
		// 비즈니스 로직 실행
		if()
	}else {
		// Error 처리
	}
}else {
 // Error 처리
}

//bad
if(deleteMember(memberDto) == E_OK){
	if(userRespository.reRegister(memberDto) == E_OK){
		// 비즈니스 로직 실행
		if()
	}else {
		// Error 처리
	}
}else {
 // Error 처리
}

//good
try {
  deleteMember(memberDto);
  userRespository.reRegister(memberDto);
  String action = "delete";
  userRespository.saveHistory(action, memberDto);
} catch(IOException | NullPointExcepiton e){
	throw new Exception(e.getMessage());
}		



형식 맞추기

속한 개발팀의 문화를 배우고 흡수하는 것이 필수입니다. 좋은 코드는 10명이 참여한 프로젝트의 코드의 main 브랜치를 봤을 때 “질서 정연하다”라고 느끼는 코드입니다. 즉, 10명이 참여하지만, 1명이 혼자 작성한 것 처럼 형식에 일관성이 있는 코드가 좋은 코드입니다.

만약, 팀 내부에 정리되어 있는 코드 스타일 규칙이 없다면, 새로운 모듈을 추가하거나 기존 코드를 고칠 때 기존에 작성되어 있는 코드를 읽어보고 어떤 스타일로 코드를 작성하고 있는지 습득하는 편이 좋습니다.


객체와 자료구조

변수를 private 하게 만드는 이유는 외부에서 변수에 의존하지 않게 만들고 싶어서 입니다. 이런 관점에서 보았을 때 Getter와 Setter를 public하게 선언하여 외부에 노출하는건 무엇인가 좀 이상해보입니다. OOP스러운 클래스는 추상 인터페이스를 제공해서 클래스 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 합니다. 즉, 객체(Object)는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개합니다. 이렇게 되면 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉽지만, 기존 객체에 새 동작을 추가하기는 어려워 질 것입니다.


OOP

기능을 모듈화 하는 이유

  1. 모듈의 재사용을 위해서입니다.
  2. 단일 책임 원칙을 부여함으로써 변경이 쉬운 코드를 작성하기 위해서 입니다.

이렇게 쉽게 변경을 계속 강조하는 이유는 고객의 요구사항은 끊임없이 변경되기 때문입니다.

객체지향 세계의 특징

객체도 사회적 동물이다

객체는 독립적인 존재가 아닙니다. 즉, 객체지향 세계에서는 공동체를 만들어 서로 상호작용하며 자신의 존재여부를 증명합니다. 상호작용은 메시지를 통해서만 가능합니다.

객체지향 세계에서 객체는 자아를 가지는 로봇이 된다.

실제 세계에서의 수동적인 존재(인간이라는 주체 없이는 동작할 수 없는 것들)를 객체지향 세계에서 객체로 구현되면 능동적인 존재가 됩니다. Theater 객체는 종업원 없이도 혼자서 티켓을 확인하고 관람객을 입장시킬 수 있습니다.

객체지향 세계에서 독점은 없다. 객체지향 세계에서는 모든 것을 가지고 있는 객체는 없습니다. 어플리케이션의 목표를 달성하기 위해서 필요한 기능을 쪼개고 적절한 객체에 분배해 객체들 간의 적절한 협력을 만들어줘야 합니다. (SRP 단일 책임의 원칙)

협력이란?

어플리케이션의 주요 기능을 구현하기 위해서 작은 단위로 쪼개진 기능을 설명하는 한줄 요약본입니다. 협력은 객체 간의 상호작용을 통해서 이루어집니다. 여기서 객체간의 상호작용은 메시지를 통해 이루어집니다.

책임이란?

협력에 참여하기 위해서 객체가 책임지고 있는 전체 기능서입니다.

역할이란?

동일한 책임을 수행하는 객체의 집합입니다. 만약, 동일한 책임을 수행하는 객체가 여러 개 있을 경우 협력을 개별적으로 만들지 않습니다. 동일한 책임을 수행하는 객체들을 대표할 수 있는 특별한 이름(역할, 추상화 객체)을 부여하고 그것들을 슬롯 형태로 고나리하면 동적으로 적절한 객체로 결합할 수 있습니다.

캡슐화

캡슐화를 통한 세부적인 내부 구현을 숨기고, 외부와의 협력은 public 인터페이스를 통해서만 하도록 합니다. 이렇게 하는 이유는 외부의 간섭을 최소화하여 객체 스스로가 상태를 관리하고 행동하는 자율적인 객체로 만들기 위해서입니다.


SOLID

SRP(Single Responsibility Pinciple) 단일 책임 원칙

객체를 변경시키는 요인은 무조건 1개여야합니다.

책임이란?

객체에 의해 정의되는 응집도(행동과 관련된 상태를 한 클래스에 모아 놓는 것) 있는 행위의 집합으로, 객체가 유지해야 하는 정보(상태)와 수행할 수 있는 행동(메서드)에 대해 추상적으로 서술한 문장입니다.

하나의 객체에 다수의 책임이 할당 되어 있는 상태에서 시간이 흐르면서 많은 변화를 갖게되면 아래와 같은 문제들이 발생하게 됩니다.

  1. 각기 다른 책임의 기능들이 강한 결합도(Coupling)를 맺게 되고, 변화가 발생했을 때 연쇄적으로 변화가 발생합니다.
  2. 적절한 관심사 분리가 되어 있지 않아서 코드의 가독성이 많이 떨어지게 됩니다. 많은 개발자들이 단일 책임 클래스가 많아지면 큰 그림을 이해하기 어려워지기 때문에 이 클래스, 저 클래스를 이동해야한다는 이유로 SRP에 대한 반론이 많다고 합니다. 하지만 시스템의 규모가 클 수록 체계적인 정리가 필수입니다.
  3. 재 사용성이 떨어집니다.
    SRP의 핵심은 어떤 변화에 의해 클래스를 변경해야한다면, 이유가 오직 하나뿐이여야합니다. 그래서 아래와 같이 클래스를 설계해야합니다.
  4. 클래스는 오직 책임(변경의 요소)를 1개만 가지도록 설계합니다.
  5. 하나의 책임이 여러 개의 클레스에 분산되어 있는 경우, 하나의 클래스에 모아서 하나의 책임에 할당해야합니다.

OCP(Open and Close Principle) 개방 폐쇄 원칙

구체화에 의존하지 말고 추상화에 의존해야합니다. 구현 이후에 다양한 변경사항이 발생하고 코드가 추가 되어야 하는데, 기존 코드는 문제 없이 잘 작동하여야합니다.

다음과 같은 규칙들을 지켜가며 OCP를 적용할 수 있습니다.

  1. 변경될 것과 변경되지 않을 것을 구분하기
    • 할인 정책은 변경된다
    • 할인 정책은 적용여부를 확인하고, 할인 금액을 계산한다. 라는 사실은 변하지 않는다.
  2. 공통된 특징을 기반으로 추상화 또는 인터페이스를 정의한다.
    • 적용 여부 판단과 할인 금액 계산은 모든 모든 할인 정책 객체에서 응답할 수 있어야 하는 메시지이다.
    • 공통으로 사용될 상태(인스턴스 변수)가 없기 때문에 여기서는 인터페이스를 선택한다.
  3. 구현에 의존하지 말고 추상화에 의존하기
    • 구현에 의존하면 변경에 유연 대응할 수 없다.

추상화, 인터페이스, 다형성이 OCP 적용을 도와주는 중요 매커니즘입니다. 이를 통해 변경의 유연성을 가질 수 있지만, 코드의 가독성 난이도가 올라간다는 단점도 있습니다. 그래서 비지니스 상황과 팀 내부 성격에 맞게 적절하게 선택해야합니다.

LSP (Liskov Substitution Priciple) 리스코프 치환 원칙

하위 타입이 완벽하게 상위 타입을 대체할 수 있다는 원칙을 뜻합니다. 이 원칙은 개방 페쇠 원칙을 제대로 사용할 수 있도록 도와주는 원칙이며 상속과 다형성을 구현하기 위 해당 원칙을 꼭 지켜야합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ParentClass {
	public void upcasting(){
		System.out.println("Up Casting");
	}
}

public class ChildClass extends ParentClass {
		@Override
		public void upcasting(){
			System.out.println("Down Casting");
		}
}

public static void main(String[] args) {
		ParentClass p = new ChildClass();
		p.upcasting(); // expect output: "Up Casting" / but output "Down Casting"
}

리스코프 치환 원칙이 위반되면 코드 변경의 유연성을 보장해주는 개방 폐쇠 원칙또한 지킬 수 없게 됩니다.

상위 타입에서 정의한 기능의 명세서(기능에 대해서 정의하고 세운 규칙)은 하위타입에서도 무조건 지켜서 구현해야 예상치 못한 에러를 방지할 수 있습니다. 만약 지켜서 구현하지 않는다면, 코드는 비정상으로 동작하게 되고 오류를 잡기위해 if/else문으로 도배가 되면서 OCP 원칙도 무너지게 될것입니다.

This post is licensed under CC BY 4.0 by the author.