보통 비지니스 로직을 구현하는 부분에 있어서는 비지니스 로직을 글이나 말로 설명하듯이 코드를 작성하는 것이 좋습니다. 물론 어플리케이션 로직과 100% 분리는 할 수 없겠지만 노력은 해야합니다. 이런 노력을 기울인다면 컴퓨터가 이해하는 로직이 아닌 사람이 이해하는 로직을 짤 수 있지 않을까 합니다.
자판기에서 커피를 뽑는 것을 생각해봅시다.
1. 자판기에 동전을 넣습니다.
2. 자판기에서 금액에 맞는 커피 종류 버튼에 불이 들어옵니다.
3. 불이 들어온 버튼을 누릅니다.
4. 커피 나오는 곳에서 불이 켜지면서 자판기에서 일정시간동안 커피를 만듭니다.
5. 커피를 만들고 나면 커피나오는 곳의 불이 꺼지게 됩니다.
아래의 경우와 비교해봅시다.
1. 자판기는 기존 금액에 동전만큼의 금액을 더합니다.
2. 커피의 종류 중 금액보다 작은 커피의 종류에 불켜짐 속성을 true로 바꿉니다.
3. 버튼을 누릅니다. 자판기는 해당 버튼의 불켜짐 속성이 true인지 false인지 비교를 합니다.
4. 불켜짐 속성이 true이면 커피 나오는 곳의 불켜짐 속성을 true로 하고, 해당 커피를 설탕 ~g과 커피 ~g, 물 ~cc를 넣어서 섞습니다.
5. 커피를 만들고 나면 커피 나오는 곳의 불켜짐 속성을 false로 바꿉니다.
위쪽이 훨씬 이해하기가 쉽지 않을까요? 물론 코드 자체는 굉장히 유사할 수 있지만 로직을 구현하는 부분에서 비지니스 설명단위로 메서드를 표현하고 메서드명을 의도에 맞게 이름을 짓는 것이 좋습니다. 예를 들어, 자판기에 동전을 넣는 것을 setMoney 또는 addMoney라고 하는 것보다 insertCoin이 훨씬 낫겠지요.
서비스 명이나 도메인에 있어서의 메서드 명은 의도가 분명히 들어나는 이름을 사용해야 할 것이고, DAO를 통해 데이터베이스에 값을 저장하는 것과 같이 어플리케이션 로직에서는 그에 맞는 이름을 사용해야 할 것입니다.
public int getWorkingDay() {
return DateUtil.diff(getStartDate(), new Date()) + 1;
}
위의 예제는 근무일을 계산하는 예제이다. 근무일 = 근무시작일과 오늘(현재날짜)의 차이 + 1의 계산공식을 통해서 구할 수 있다.
이럴때 테스트하기란 참 어렵다. 오늘은 항상 바뀌기 때문이다. 오늘 성공한 테스트케이스가 내일이면 분명히 실패할 것이다. 물론 사용하는데 있어서 전혀 문제가 없지만 어떠한 특정 계산을 하는 클래스가 시간에 종속이 되면 테스트도 힘들뿐더러 재사용하기도 난감한 경우가 있다.
이를 위해서 다음과 같은 절차를 따른다.
1. new Date()와 같이 시간이 들어가는 부분이 있다면 DI를 이용해서 기준일을 넣는다.
public class Employee { private Date baseDate; // 기준일
private Date startDate; // 근무시작일
public Date setDate(Date baseDate){
this.baseDate = baseDate;
}
}
2. new Date()가 들어가는 부분을 baseDate로 바꾼다.
public int getWorkingDay() {
return DateUtil.diff(getStartDate(), getBaseDate()) + 1;
}
위의 절차대로 바꾸면 테스트하기 쉬운구조가 된다. baseDate만 넣으면 항상 테스트결과는 동일하다. 그럼 baseDate는 언제 값을 설정하는가? 서비스 컴포넌트에서 Entity도메인을 생성하는 시점에 넣어주면 된다.
다음 예제를 보라.
public class EmployeeServiceImpl implements EmployeeService{
...
public int readWorkingDayByEmployeeId(Long employeeId){ Employee e = readEmployee(employeeId);
e.setBaseDate(new Date());
return e.getWorkingDay();
}
...
}
즉, 서비스 컴포넌트에서 실제 시간을 주입하고, 엔티티 컴포넌트에서는 시간에 독립적이어야한다. 엔티티 컴포넌트를 조립하여 서비스 컴포넌트를 만들때 영업일 개념이 들어가게 되면 처음에 만든 예제와 같이 되어있다면 재활용성이 매우 떨어지기 때문이다.
마지막 예제에서도 좀 더 손을 보면 좋을듯하다. 지금은 new Date()를 통해서 현재날짜를 넣고있지만 상황에 따라서는 영업일 개념이 들어가야할 것이다. 즉, new Date()로 구현하지 말고 영업일을 담당하는 클래스 또는 컴포넌트를 만들고 그곳에서부터 기준일을 가져와야 할 것이다. 그렇게되면 일관성있는 날짜 정책을 수립할 수 있다.
더 좋은 방법은 차라리 날짜를 파라미터로 받는 것이다 ^^;;
ps. 사실 기준일을 setBaseDate로 넣어주는 것을 잊어버릴 수 있다. 생성자에서 받아도 좋으나 가장 좋은 방법은 getWorkingDay에 파라미터로 baseDate를 받고, private 변수 자체를 없애는 것이다. 즉, getWorkingDay(Date baseDate) 이런식으로...
public final static String RED = "red"; public final static String GREEN = "green"; public final static String BLUE = "blue";
}
Enum 타입
public Enum Color {
RED, GREEN, BLUE
}
Enum 타입은 유일성을 보장하기 때문에 되도록이면 Enum 타입을 권장한다. 만약 JDK버전이 1.5이전 버전이라면 Enum 타입을 사용할 수 없다. 이럴경우 유일성을 보장하기 위해서는 다음과 같이 구현하면 된다.
Enum 타입처럼 구현된 클래스
public class Color {
public final static Color RED = new Color("red");
public final static Color GREEN = new Color("green");
public final static Color BLUE = new Color("blue");
null 및 예외처리 규칙
1. 메서드를 정의하는 부분에서 null일때 예외를 던질 것인가?
2. 메서드를 사용하는 부분에서 null을 확인하고 처리할 것인가?
3. 1번과 2번을 동시에 사용하지는 마라. (둘 중 하나 선택)
4. RuntimeException의 경우 컴파일시 체크하지 않으므로 잘 사용하면 코드를 줄일 수 있다.
5. 약정에 의한 프로그래밍을 할 것인지, 방어적 프로그래밍을 할 것인지에 대한 것은 고객의 요구나 업무 지침에 따르도록 한다.
약정에 의한 프로그래밍
- 트랜잭션의 양쪽 대상이 무슨 행동이 무슨 행위를 발생시키는지 알고 있다는 가정하에 프로그래밍을 하는 것
- 예외 상황에 대한 특별한 처리를 하지 않는다.(컴파일때 처리를 하지 않아도 컴파일 에러가 발생하지 않는다.)
- 오류 발생시 대부분 null이나 0, -1, RuntimeException 종류의 예외 상황 등을 반환한다.
...
public Object getProperty(String property){
...
// properties는 Map형태로 속성을 저장하는 컬랙션이다.
// 컴파일시는 필요 없지만 실행이 발생하는 RuntimeException을 던진다. 호출한 쪽에서 처리해야한다.
if(properties == null){
throw new RuntimeException("이 유닛은 속성이 없습니다.");
}
...
// value가 null이라도 그대로 반환한다.
Object value = properties.get(property);
return value;
}
...
방어적 프로그래밍
- 잘못될 것을을 찾아서, 문제 상황을 회피할 수 있도록 폭넓게 테스트한다.
- 예외 상황에 대한 처리를 내부적으로 구현하거나 예외 처리를 필수적으로 하기 위해서 컴파일때 에러가 발생하도록 한다.
- 오류 발생시 대부분 비어있는 객체나 컴파일시 예외처리가 필요한 Exception 종류의 예외 상황 등을 반환한다.
...
public Object getProperty(String property) throws IllegalAccessException{
...
// properties는 Map형태로 속성을 저장하는 컬랙션이다.
// 예외처리 필요한 Exception을 던진다.
if(properties == null){
throw new IllegalAccessException("이 유닛은 속성이 없습니다.");
}
...
// 빈 객체를 반환한다. 실제로 Object를 비어있는 객체로 만드는 경우는 없다.
Object value = properties.get(property);
if(value == null){
value = new Object();
}
return value;
}