객체지향의 중요성을 알게 되고 이러한 객체지향을 잘 적용하기 위해 중요한 9가지 원칙을 정리하기 위해 글을 작성하였다.
객체지향 생활 체조 원칙 9가지
- 한 메서드에 오직 한 단계의 들여 쓰기만 한다.
- else 예약어를 사용하지 않는다.
- 모든 원시 값과 문자열을 포장한다.
- 한 줄에 점을 하나만 찍는다.
- 줄여 쓰지 않는다.
- 모든 엔티티는 작게 유지한다.
- 3개 이상의 인스턴스 변수를 가진 클래스를 사용하지 않는다.
- 일급 컬렉션을 쓴다.
- getter/setter/프로퍼티를 사용하지 않는다.
1. 한 메서드에 오직 한 단계의 들여 쓰기만 한다.
(One level of indentation per method)
아래 예시를 보자.
public class Map {
public String map() {
StringBuilder sb = new StringBuilder();
int raw = 5;
int repeat = 10;
// 0
for (int i = 0; i < raw; i++) {
//1
for (int j = 0; j < repeat; j++) {
// 2
sb.append("Hello");
}
sb.append("\n");
}
return sb.toString();
}
}
위 코드를 첫 번째 규칙을 활용하여 바꾸면 아래와 같다.
public class Map {
public String map() {
StringBuilder sb = new StringBuilder();
int raw = 5;
int repeat = 10;
repeatHelloAndNewLine(sb,raw,repeat);
return sb.toString();
}
private void repeatHelloAndNewLine(StringBuilder sb, int raw, int repeat) {
for (int i = 0; i < raw; i++) {
repeatHello(sb, repeat);
sb.append("\n");
}
}
private void repeatHello(StringBuilder sb, int repeat) {
for (int i = 0; i < repeat; i++) {
sb.append("Hello");
}
}
}
코드에 많은 들여 쓰기가 있다면, 가독성이 떨어진다. 이는 한눈에 보기 어렵고 복잡해진다.
또한 들여쓰기가 여러 개 있다면 해당 메서드가 여러 일을 하고 있을 가능성이 높다.
작게 나눌수록 재사용성이 높아지고 직관적이게 된다.
2. else 예약어를 사용하지 않는다.
(Don't use the else keyword)
아래 예시를 보자
public boolean isOpen(int hour) {
boolean open;
if (9 < hour && hour < 22) {
open = true;
} else {
open = false;
}
return open;
}
위의 코드를 두 번째 규칙을 활용해 바꾸면 아래와 같다.
public boolean isOpen(int hour) {
if (9 < hour && hour < 22) {
return true;
}
return false;
}
else를 사용할 경우 가독성이 떨어지게 된다. else문을 early return 문을 통해 더욱 간결하게 해결할 수 있다.
3. 모든 원시 값과 문자열을 포장한다.
(Wrap all primitives and string)
아래 예시를 보자.
public class Human {
private string name;
private int age;
public User(string name, int age) {
validateName(name);
validateAge(age);
this.name = name;
this.age = age;
}
private void validateName(String name) {
if (name.length() < 2) {
throw new IllegalArgumentException("이름은 두 글자 이상이어야 합니다.");
}
}
private void validateAge(int age) {
if (age <= 0) {
throw new IllegalArgumentException("나이는 1살부터 시작합니다.");
}
}
}
위의 코드를 세 번째 규칙을 적용하여 바꾸면 아래와 같다.
public class Human {
private Name name;
private Age age;
public User(String name, String age) {
this.name = new Name(name);
this.age = new Age(age);
}
}
public class Name {
private String name;
public Name(String name) {
if (name.length() < 2) {
throw new IllegalArgumentException("이름은 두 글자 이상이어야 합니다.");
}
this.name = name;
}
}
public class Age {
private int age;
public Age(int age) {
if(age <= 0) {
throw new IllegalArgumentException("나이는 1살부터 시작합니다.");
}
}
}
만약 원시값을 그대로 사용한다면 나이, 이름에 대한 책임도 Human이 전부 가지게 된다.
이러면 Human의 필드가 많아지면 그만큼 더 많은 책임을 가지게 된다.
따라서 Name, Age 객체로 분리하여 객체 스스로 관리하고 책임을 갖도록 할 수 있다.
이를 통해 코드의 유지 보수가 증가 된다.
4. 한 줄에 점을 하나만 찍는다.
(Use only one dot per line)
아래 코드를 보자.
public String getMoney() {
return this.member.getWallet().getMoney();
}
위 코드를 네 번째 규칙을 활용하여 바꾸면 아래와 같다.
public String getMoney() {
Wallet wallet = this.member.getWallet();
return wallet.getMoney();
}
한 곳에서 점이 두 개 이상이 있다면 다른 두 객체를 동시에 조작할 가능성이 있다.
즉 캡슐화를 깰 수 있다.
5. 줄여 쓰지 않는다.
(Don't abbreviate)
아래 코드를 보자
public class Member {
void printMemberInfo() {
int m = 10000;
String n = "java"
System.out.println("m = " + m +" n = " +n);
}
}
위 코드를 다섯 번째 규칙을 활용하여 바꾸면 아래와 같다.
public class Member {
void printInfomation() {
int money = 10000;
String name = "java"
System.out.println("money = " + money +" name = " +name);
}
}
단축은 오히려 가독성을 떨어트린다.
printMemberInfo의 경우 문맥상 Member가 겹치므로 제거한다.
6. 모든 엔티티는 작게 유지한다.
(Keep all entities small)
클래스는 50줄, 패키지는 파일 10개를 넘기지 않아야 합니다.
긴 파일은 가독성이 떨어지고, 재사용하기가 어렵기 때문에, 엔티티들을 잘게 쪼개 세분화시키는 게 중요하다.
7. 3개 이상의 인스턴스 변수를 가진 클래스를 사용하지 않는다.
(No classed with more than two instance variables)
아래 코드를 보자
public class Member {
private String firstName;
private String lastName;
private int age;
public Jamie(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
이를 일곱 번째 규칙을 활용하여 바꾸면 아래와 같다.
public class Member {
private Name name;
private Age age;
public Jamie(Name name, Age age) {
this.name = name;
this.age = age;
}
}
한 클래스에 인스턴스 변수를 많이 선언하면 많은 책임을 가지게 된다. SOLID 규칙에서 SRP 규칙을 위배하게 된다.
또한 인스턴스 변수가 많아지면 클래스의 복잡성이 증가한다. 이로 인해 유지보수가 어려워진다.
따라서 공통속성이 있는 인스턴스 변수의 경우 하나의 객체로 만드는 방법 등을 사용하여 쉽게 쪼갤 수 있다.
8. 일급 컬렉션을 쓴다.
(First Class Collection)
일급컬렉션은 collection을 포장하고, 그 외 다른 변수가 없는 클래스를 의미한다.
아래 코드를 보자.
Map<String, Integer> map = new HashMap<>();
map.put("java", 10000);
map.put("cpp",2500);
map.put("python", 5000);
위 코드를 여덟 번째 규칙을 활용하여 바꾸면 아래와 같다.
public class BookCost {
private Map<String, String> costs;
public BookCost(Map<String, Integer> costs) {
this.costs = costs;
}
}
비즈니스에 종속적인 자료구조가 된다. 해당 컬렉션에서 필요한 모든 로직은 일급 컬렉션에서 구현하기 때문이다.
private final을 선언하여 Collection을 생성해 주고, 생성자를 통해 생성해 주면 재할당이 불가능하므로 불변 컬렉션이 된다.
일급 컬렉션 내에서 상태와 행위를 모두 관리하므로, 해당 일급 컬렉션을 사용하는 사용 클래스에서는 상태와 로직을 한 곳에서 관리할 수 있게 된다.
일급 컬렉션 생성 시 Class명을 명명하게 되는데, 이렇게 되면 검색도 편하고 명확한 표현도 가능하게 된다.
9. getter/setter/프로퍼티를 사용하지 않는다.
(No Getters/Setters/Properties)
아래 코드를 보자.
// Money
private int money;
public void setMoney(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
money.setMoney(money.getMoney() + MY_MONEY);
위 코드를 아홉 번째 규칙을 활용해 바꾸면 아래와 같다.
// Money
public void addMoney(int money) {
this.money += money;
}
// Usage
money.addMoney(MY_MONEY);
객체의 상태를 가져오는 접근자를 사용하는 것은 괜찮지만, 객체 바깥에서 그 결과값을 사용해 객체에 대한 결정을 내리면 안 된다.
객체의 상태에 대한 결정은 어떤 것이든 그 객체 안에서 이루어져야 한다.
즉 단순하게 getter, setter를 사용하는 것이 아닌 메시지를 통해 객체 내에서 동작을 결정하도록 해야 한다.
또한 더 명확한 메서드명을 주어 가독성을 높이는 것도 중요하다.
마무리
대부분이 이미 학습을 하며 아는 내용이었다. 하지만 스스로에게 이를 잘 지키고 있냐고 물어본다면 그러지 못하였다.
쉬워 보이는 내용이지만 이를 모두 지키면서 코딩하는 것은 쉽지 않다. 또한 스스로 이를 인지하지 못하고 작성할 때도 있다.
객체지향 생활체조의 이름답게 이를 잘 적용하기 위해 꾸준히 인식하고 연습해야겠다.
'좋은 개발자가 되기' 카테고리의 다른 글
객체지향의 사실과 오해(4장) (0) | 2023.04.25 |
---|---|
객체지향의 사실과 오해(3장) (2) | 2023.04.17 |
객체지향의 사실과 오해(2장) (0) | 2023.04.11 |
객체지향의 사실과 오해 (1장) (0) | 2023.04.05 |
자바 코드 컨벤션 (2) | 2023.03.29 |