KISS 원칙
- Keep It Simple and Stupid
- Keep It Short and Simple
- Keep It Simple and Straight
적은 줄 수의 코드가 더 간단하지 않다
ip 주소 (ipAddress) 변수가 유효한지 검사하는 코드를 작성하는 예시를 비교해보자.
1) 정규 표현식을 사용한 예제
public boolean isValidIpAddressV1(String ipAddress) {
if (StringUtils.isBlank(ipAddress)) {
return false;
}
String regex = "^(\\d{1,2}|1\\d{2}|2[0-4]\\d|25[0-5])\\."
+ "(\\d{1,2}|1\\d{2}|2[0-4]\\d|25[0-5])\\."
+ "(\\d{1,2}|1\\d{2}|2[0-4]\\d|25[0-5])\\."
+ "(\\d{1,2}|1\\d{2}|2[0-4]\\d|25[0-5])$";
return ipAddress.matches(regex);
}
2) 기성 유틸클래스를 사용한 예제
public boolean isValidIpAddressV2(String ipAddress) {
if (StringUtils.isBlank(ipAddress)) {
return false;
}
String[] ipUnits = StringUtils.split(ipAddress, '.');
if (ipUnits.length != 4) {
return false;
}
for (int i = 0; i < 4; ++i) {
int ipUnitIntValue;
try {
ipUnitIntValue = Integer.parseInt(ipUnits[i]);
} catch (NumberFormatException e) {
return false;
}
if (ipUnitIntValue < 0 || ipUnitIntValue > 255) {
return false;
}
if (i == 0 && ipUnitIntValue == 0) {
return false;
}
}
return true;
}
코드 길이로 보면 정규 표현식을 사용한 코드가 훨씬 짧지만, 더 단순하다고 말할 수 없다.
정규표현식에 익숙하지 않은 개발자라면 읽고 유지보수하기 어려울 수 있고, 예외 케이스나 버그가 발생할 확률도 더 높다.
코드가 짧다고 해서 간단한 것은 아니고, 논리 복잡성/구현 난이도/가독성 까지 모두 고려하여 간단한 코드인지 판단하자.
복잡한 코드가 반드시 KISS 원칙을 위반하는 것은 아니다
KMP 알고리즘은 효율적이지만 복잡하고 가독성도 떨어진다.
특정 문자열 텍스트에서 일치하는 가장 긴 텍스트를 찾는 문제를 해결해야 한다고 할 때,
1) 문자열 일치 알고리즘이 시스템 병목을 가져와 성능이 높은 KMP 알고리즘을 사용해야 한다면 KISS 원칙에 위배되지 않는다.
2) 간단하고 짧은 문자열을 대상으로 KMP 알고리즘을 사용한다면 KISS 원칙에 위배된다.
즉, 동일한 코드(알고리즘)을 사용한다고 해도 사용하는 배경과 시나리오에 따라 KISS 원칙에 위배될 수도 있고, 아닐 수도 있다.
KISS 원칙을 만족하는 코드 작성 방법
- 복잡한 정규표현식을 사용하지 말자.
- 바퀴를 다시 발명(reinvent the wheel) 하는 대신 기존 라이브러리를 사용하자.
- 과도하게 최적화 하지 말자. (코드를 최적화 하기 위해 산술 연산 대신 비트 연산을 사용하거나 if-else 대신 복잡한 조건문을 사용하지 말자.)
YAGNI 원칙
You Aren't Gonna Neet It
현재 사용되지 않는 기능을 설계하거나 현재 사용되지 않는 코드를 작성하지 않는다는 원칙
예를 들어, Gradle/Maven 에 현재 사용하지 않는 공통 라이브러리를 다 가져와 미리 적용시키는 것은 YAGNI 원칙에 위배된다.
KISS 원칙 = 설게와 코드를 간단히 작성하고 유지할 것
YAGNI 원칙 = 현재 필요하지 않은 것은 미리 하지 말라는 것 (그렇다고 확장성을 고려하지 말라는 의미는 아님)
DRY 원칙
코드 논리의 중복
흔히 '중복' 해서 코드를 작성하지 말라고 하는 것을 동일한 모양새(?)의 코드를 프로젝트 내에서 여러번 사용하지 말라는 뜻으로 이해하지만, 동일한 코드를 작성했다고 해서 꼭 DRY 원칙에 위배되는 것은 아니다.
예를 들어서,
public class UserAuthenticator {
public void authenticate(String username, String password) {
if (!isValidUsername(username)) {
// ...
}
if (!isValidPassword(password)) {
// ...
}
}
private boolean isValidUsername(String username) {
// username 이 유효한지 판단하는 논리
// blank 가 아니어야 하고, 알파벳 소문자여야 하고... 등등
}
private boolean isValidPassword(String password) {
// password 가 유효한지 판단하는 논리
// 현재 username isValidUsername() 논리와 동일하다고 가정
}
}
와 같은 코드가 있다고 할 때, isValidUsername() 과 isValidPassword() 내의 코드 논리가 동일하므로 이를 하나의 함수로 합치면?
처음에는 코드 중복을 줄이는 옳은 의사결정을 했다고 생각할 수 있겠으나,
추후에 password를 판단하는 로직이 달라진다면 합쳤던 것을 다시 분리해야 하는 문제가 생기게 된다.
모양은 동일한 코드 논리를 사용하는 것 같아도 username 을 판단하는 로직과 password를 판단하는 로직은 엄연히 다른 것이므로 위의 경우에는 DRY 원칙을 위반한 것으로 보지 않는다. (오...! 내가 평소에 너무나 고민하던 포인트...)
기능적(의미론적) 중복
코드 논리의 중복과 반대로, 프로젝트에 다음과 같은 코드들이 있다고 해보자.
public boolean isValidIp(String ipAddress) {
// 정규표현식을 이용한 ipAddress 판단 로직
}
public boolean checkIfIpValid(String ipAddress) {
if (StringUtils.isBlank(ipAddress)) {
return false;
}
String[] ipUnits = StringUtils.split(ipAddress, '.');
if (ipUnits.length != 4) {
return false;
}
if (int i = 0; i < 4; ++i) {
try {
// 각 자리 숫자로 변환
}
// 변환된 자리 수 유효성 체크
}
}
ipValidIp, checkIfIpValid 함수는 로직도, 함수 이름도 다르지만 동일한 기능을 하는 함수를 중복해서 만들어 놓은 것이다. (아마... 프로젝트 코드를 잘 안 읽은 개발자들이 서로 다른 함수를 만들어 놓은 것) 이런 경우에는 DRY 원칙을 위배한다고 할 수 있다.
코드 실행의 중복
public class UserService {
private UserRepo userRepo;
public User login(String email, String password) {
boolean existed = userRepo.checkIfUserExisted(email, password);
// ...
User user = userRepo.getUserByEmail(email);
// ...
return user;
}
public class UserRepo {
public boolean checkIfUserExisted(String email, String password) {
if (!EmailValidation.validate(email)) {
// ...
}
if (!EmailValidation.validate(password)) {
// ...
}
}
public User getUserByEmail(String email) {
if (!EmailValidation.validate(email)) {
// ...
}
}
}
}
위의 코드에서 다음과 같은 로직이 중복으로 실행되고 있다.
1) !EmailValidation.validate(email)
2) login() 함수는 checkIfUserExisted() 를 호출 후 getUserByEmail() 함수를 호출하여 불필요하게 DB에 두 번 접근
코드 중복 실행은 DRY 원칙에 위배되므로 다음과 같이 수정하는 것이 좋다.
1) !EmailValidation.validate(email) 함수는 login() 함수 내로 이동
2) getUserByEmail() 호출로 DB에 한 번만 접근
public class UserService {
private UserRepo userRepo;
public User login(String email, String password) {
if (!EmailValidation.validate(email)) {
// ...
}
if (!EmailValidation.validate(password)) {
// ...
}
User user = userRepo.getUserByEmail(email); // DB 한 번만 접근
// ...
return user;
}
public class UserRepo {
public boolean checkIfUserExisted(String email, String password) {
// ...
}
public User getUserByEmail(String email) {
// ...
}
}
}
LOD
높은 응집도와 낮은 결합도
클래스에 대한 시나리오로 생각해보면,
높은 응집도는 유사한 기능은 동일한 클래스로, 유사하지 않은 기능은 다른 클래스로 분리함을 의미한다.
낮은 결합도는 클래스 간의 의존성이 명확하고 단순해야 함을 의미한다.
응집도와 결합도는 완전히 독립적이지 않고, 응집도가 높으면 결합도는 낮아지고 / 결합도가 낮으면 응집도는 높아진다.
클래스가 단일 책임의 원칙을 따를 때 응집도는 높아지고 결합도는 낮아진다.
LOD 정의
Law Of Demeter, 최소 지식의 원칙
모든 유닛은 자신과 밀접하게 관련된 유닛에 대해서 제한된 지식만 알아야 함을 의미한다.
LOD 에 대한 정의는 사람마다 경험한 바에 따라 다를 수 있는데, 저자는 다음과 같이 두 가지 내용으로 정리한다.
1) 직접 의존성이 없어야 하는 클래스 사이에서는 반드시 의존성이 없어야 한다.
2) 의존성이 있는 클래스는 필요한 인터페이스에만 의존해야 한다.
* 예제 코드 이해 잘 못해서 스터디 발표 잘 들어볼 것
'BE 📙 > 디자인패턴의 아름다움' 카테고리의 다른 글
| [CH 05.01, 05.02] 리팩토링, 단위테스트 (4) | 2025.08.12 |
|---|---|
| [CH03.01] 설계 원칙 - 단일 책임 원칙 (1) | 2025.08.05 |
| [CH02.09] 상속보다 합성 (1) | 2025.08.04 |
| [CH01] 개요 (5) | 2025.07.21 |
댓글