요구사항에 맞게 프로젝트를 시작하여 개발을 하다보면, 초기의 요구사항이 그대로 마지막까지 변하지 않고 유지되는 경우는 거의 없습니다. 개발 도중, 개발 후에 요구사항이 시시각각 변화하는 경우가 많은데요. 이런 요구사항의 변화가 오더라도 손쉽게 적용할 수 있는 코드를 작성하는 것이 중요합니다.
"변하는 부분과 변하지 않는 부분을 분리하는 것" - 소프트웨어 주요 설계 원칙
📌 동작 파라미터화(behavior parameterization)
동작 파라미터화는 아직 어떻게 실행할 것인지 결정하지 않은 코드 블럭을 의미합니다. 다른 말로는 행위 매개변수화 라고도 합니다.
동작 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응이 가능합니다.
⌛ 상황
어플리케이션에서 사용자의 '대표성'을 띄는 속성으로 이름, 닉네임, 이메일 등이 사용된다. 우리는 위 3가지 속성으로 사용자의 나이를 반환하는 함수를 구현하려고 한다. <단, 3가지 속성 모두 유일성을 갖는다고 가정한다.>
초기 요구사항 : "사용자의 이름으로 나이를 알려주세요"
2번째 요구사항 : "사용자의 닉네임으로 나이를 알려주세요"
3번째 요구사항 : "사용자의 이메일로 나이를 알려주세요"
초기 요구사항 : "사용자의 이름으로 나이를 알려주세요"
public int getAgeByName(List<User> users, int name) {
int result = 0;
for (User user : users) {
if (user.getName.equals(name)) {
result = user.getAge();
}
}
return result;
}
2번째 요구사항 : "사용자의 닉네임으로 나이를 알려주세요"
public int getAgeByNickname(List<User> users, int nickname) {
int result = 0;
for (User user : users) {
if (user.getNickname.equals(nickname)) {
result = user.getAge();
}
}
return result;
}
3번째 요구사항 : "사용자의 이메일로 나이를 알려주세요"
public int getAgeByEmail(List<User> users, int email) {
int result = 0;
for (User user : users) {
if (user.getEmail.equals(email)) {
result = user.getAge();
}
}
return result;
}
메서드가 많아지고 모든 요구사항에 맞는 메서드를 모두 구현하였다고 뿌듯해하면 안됩니다. 우리가 짠 코드를 유심히 살펴봅시다. 👀
위 3가지 메서드의 구현부는 중복되는 코드가 너무 많습니다. 이 점은 소프트웨어 공학 원칙 중 하나인 DRY(Don't Repeat Yourself)을 위반하는 것입니다.
DRY(Don't Repeat Yourself)
똑같은 일을 두번하지 않는다. 중복되는 함수나 코드는 하나의 공통의 콤포넌트에 넣고 사용한다. 큰 시스템을 여러 조각으로 나누고 서로 참조한다.
소규모 시스템일 경우엔 프로그램의 복잡도가 크지 않기 때문에 코드를 이해하기 수월한 반면에 대규모 시스템으로 갈수록 복잡도는 기하급수적으로 높아지게 됩니다.
이럴 경우 복잡도를 최대한 줄여야 개발 및 유지보수 비용 절감을 할 수 있습니다.
우리는 위에서 짠 코드와 같이 파라미터를 상황에 따라 바꾸는 방식이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 방법으로 전략 디자인 패턴을 사용할 것입니다.
전략 디자인 패턴
알고리즘(전략)을 캡슐화하는 알고리즘 패밀리를 정의한 후 런타임에 알고리즘을 선택하는 기법
먼저 사용자를 파라미터로 받고 조건에 맞는지 아닌지 반환하는 메서드가 있는 인터페이스를 정의하고 구현해봅시다.
public interface UserPredicate{
boolean test(User user);
}
처음엔 되게 많이 당황했습니다.. 왜냐하면 대부분 인터페이스를 구현하는 클래스에선 상수 값에 대한 필터를 취하는 형태였기 때문입니다. 하지만 우리의 요구사항은 닉네임, 이름, 이메일 등의 변수 값이 주어져야 합니다.
이를 해결하기 위해선 인터페이스의 구현체(Class)에서 멤버 변수를 갖게하여 생성자를 통해 초기화되게 하여야 합니다.
**// 첫번째 요구사항 : 이름**
public class NamePredicate implements UserPredicate{
private String name;
public NamePredicate(String name){
this.name = name;
}
@Override
public boolean test(User user){
return name.equals(user.getName());
}
}
**// 두번째 요구사항 : 닉네임**
public class NicknamePredicate implements UserPredicate{
private String nickname;
public NicknamePredicate(String nickname){
this.nickname = nickname;
}
@Override
public boolean test(User user){
return nickname.equals(user.getNickname());
}
}
**// 세번째 요구사항 : 이메일**
public class EmailPredicate implements UserPredicate{
private String email;
public EmailPredicate(String email){
this.email = email;
}
@Override
public boolean test(User user){
return email.equals(user.getEmail());
}
}
위와 같이 구현체를 구현하였으면 다음과 같이 맨처음의 코드를 개선한 코드를 작성할 수 있습니다.
public int getAge(List<User> users, UserPredicate u){
int result = 0;
for(User user : users){
if(u.test(user)){
result = user.getAge();
}
}
return result;
}
**// in Main**
EmailPredicate emailPredicate = new EmailPredicate**("dia0312@naver.com");**
NamePredicate namePredicate = new NamePredicate("손흥민");
int age1 = getAge(users, emailPredicate);
int age2 = getAge(users, namePredicate);
위 코드 에서 emailPredicate, namePredicate 을 생성할 때 더미 데이터를 넣어줬지만, 클라이언트에서 받은 데이터를 전달받는 방식으로도 사용할 수 있을 것입니다.
변화하는 요구사항에 맞게 UserPredicate 인터페이스를 구현하는 여러 클래스를 정의하고 인스턴스화하는 것도 일입니다..
Java에서는 클래스 선언과 인스턴스화를 동시에 수행할 수 있도록 익명클래스(anonymous class) 기법을 제공합니다.
이러한 익명한 클래스를 이용하면 위의 코드 양을 효과적으로 줄일 수 있습니다.
getAge(users, new EmailPredicate("dia0312@naver.com") {
@Override
public boolean test(User user) {
return this.getEmail().equals(user.getEmail());
}
});
단, EmailPredicate
클래스에 email
멤버 변수에 대한 GETTER
을 구현해야 합니다.
public class EmailPredicate implements UserPredicate{
private String email;
public EmailPredicate(String email){
this.email = email;
}
@Override
public boolean test(User user){
return email.equals(user.getEmail());
}
public String getEmail() {
return email;
}
}
우리는 함수형 인터페이스일 경우 람다 표현식을 적용할 수 있습니다. 람다 표현식을 이용하면 위의 코드보다 더욱 간결하게 코드를 작성할 수 있습니다.
getAge(users, user -> "dia0312@naver.com".equals(user.getEmail()));
동작 파라미터화 (behavior parameterization)
자바와 행위 매개변수화(Behavior Parameterization)