heo-jae-won@home:~$

Java_OOP

클래스 나누기

  • 만약, switch문에 따라 분기 하는 코드가 많다면, 그것은 클래스가 잘 나눠지지 않았다는 신호다.
class Figure {
    enum Shape {RECTANGLE, CIRCLE};

    final Shape shape;

    double length;
    double width;

    double radious;

    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch(shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default :
                throw new AssertionError();
        }
    }
}
  • 아래처럼 클래스를 나눠줘야 한다.
  • Figure를 바탕으로, Circle과 Rectangle이 만들어진다.
abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    double area() {
        return Math.PIU * (radius * radius);
    }
}

class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    double area() {
        return (length * width);
    }
}
  • 그러면 앞으로 다른 객체를 추가할 때 훨씬 편리하다.
class Square extends Rectangle {
    Square(double side) {
        super(side, side);
    }
}

클래스 나누기2

  • interface, extends, abstract 등이 없이 만들어 보려면 아래와 같이 노가다를 거친다.
  • novice class에 쓰는 name, profession, stat field는 사실 magician 등의 직업 class에서도 공유한다.
public class Novice {
	String name;
	String profession;
	Stat1 stat;
	
	public Novice(String name) {
		this.name = name;
		this.profession = "초보자";
		this.stat = new Stat1();
	}

	public void doSkill() {
		System.out.println("아직 스킬이 없습니다");
	}

	public void doBasicAttack() {
		System.out.println("기본공격");
		
	}
	
	@Override
	public String toString() {
		return "[ID: " + this.name 
				+"(" + this.profession + "), stat: "
				+ "힘(" + this.stat.him + "), "
				+ "민(" + this.stat.min + "), "
				+ "지(" + this.stat.ji + ")]";
	}
	
}
  • 하지만 부모를 extends 하지 않았기 때문에 여기도 그대로 써줘야 한다.
  • 또한 자유 전직 이벤트 발생 시, 이미 전직한 class가 Magician으로 바뀌는 것이다.
    • 따라서 각 직업 별 생성자를 전부 미리 마련해둬야 한다.
    • 직업별 정보를 출력하는 toString도 부모에 있으면 부모걸 그대로 쓰면 편한데, 개별 class마다 구현해야 한다.
  • skill, attack 또한 개별 class에 묶이게 된다.
    • 누가 오타를 내서 method를 dSkill로 만들면 Magician class는 타 class와 다르게 dSkill()이 되어버린다.
public class Magician {
	String name;
	String profession;
	Stat1 stat;
	
	public Magician(Novice novice) {
		this.name = novice.name;
		this.profession = "마법사";
		this.stat = novice.stat;
	}
	
	public Magician(Knight knight) {
		this.name = knight.name;
		this.profession = "마법사";
		this.stat = knight.stat;
	}
	
	public Magician(Thief thief) {
		this.name = thief.name;
		this.profession = "마법사";
		this.stat = thief.stat;
	}

	public void doSkill() {
		System.out.println("메테오");
		
	}

	public void doBasicAttack() {
		System.out.println("매직 애로우 공격");
		
	}
	
	@Override
	public String toString() {
		return "[ID: " + this.name 
				+"(" + this.profession + "), stat: "
				+ "힘(" + this.stat.him + "), "
				+ "민(" + this.stat.min + "), "
				+ "지(" + this.stat.ji + ")]";
	}

}
  • Knight와 Thief도 불필요한 구현이 많아진다
  • 활용할 때도 extends, interface 등을 고려하지 않고 만들었으니 다형성이 부족하다.
  • 아래처럼 각 class 별로 전부 참조를 담는 변수를 만들어 null로 만들었다가 뺐다가 관리해야 한다.
  • 또한 해당 class의 변수가 null인지 아닌지 확인하는 분기문을 통해 logic을 전개해야 한다.
public static void main(String[] args) {
		Novice novice = null;
		Knight knight = null;
		Magician magician = null;
		Thief thief = null;
		try (Scanner scan = new Scanner(System.in)) {
			while(true) {
				GameUtil.printMenu();
				int menu = scan.nextInt();
				
				switch (menu) {
					case 1:
						System.out.println("캐릭터를 생성합니다.");
						System.out.print("사용하실 아이디를 입력해주세요.");
						String nickname = scan.next(); //nextLine쓰면 이상하게 씹힘.
						System.out.println("스탯을 부여합니다.");
						while (true) {
							novice = new Novice(nickname);
							knight = null;
							magician = null;
							thief = null;
							GameUtil.printStatInfo(novice);
							System.out.println("스탯을 다시 받으시겠습니까? (y/n)");
							String choice = scan.next(); //nextLine쓰면 이상하게 씹힘.
							if (choice.equals("N") || choice.equals("n")) {
								break;
							} 
						}
						System.out.println("현재 정보로 저장합니다.");
						break;
					case 2:
						if (novice != null) {
							System.out.println(novice.toString()); 
						} else if (knight != null) {
							System.out.println(knight.toString()); 
						} else if (magician != null) {
							System.out.println(magician.toString()); 
						} else if (thief != null) {
							System.out.println(thief.toString()); 
						} else {
							System.out.println("먼저 캐릭터를 생성해주세요");
						}
						break;
					case 3:
						if (knight != null || thief != null || magician != null) {
							System.out.println("이미 전직이 완료되었습니다.");
							break;
						}
						if( novice == null) {
							System.out.println("먼저 캐릭터를 생성해주세요");
							break;
						}
						GameUtil.printProfessionMenu();
						int professionMenu = scan.nextInt();
						switch (professionMenu) {
							case 1:
								System.out.println("기사로 전직합니다.");
								knight = new Knight(novice);
								novice = null;
								thief = null;
								magician = null;
								break;
							case 2:
								System.out.println("도적으로 전직합니다.");
								thief = new Thief(novice);
								novice = null;
								magician = null;
								knight = null;
								break;
							case 3:
								System.out.println("마법사로 전직합니다.");
								magician = new Magician(novice);
								novice = null;
								knight = null;
								thief = null;
								break;
							default:
								System.out.println("제대로된 거 쓰셈");
						}
						break;
					case 4:
						if (novice != null) {
							novice.doBasicAttack();
						} else if (knight != null) {
							knight.doBasicAttack();
						} else if (magician != null) {
							magician.doBasicAttack();
						} else if (thief != null) {
							thief.doBasicAttack();
						} else {
							System.out.println("먼저 캐릭터를 생성해주세요");
						}
						break;
					case 5:
						if (novice != null) {
							novice.doSkill();
						} else if (knight != null) {
							knight.doSkill();
						} else if (magician != null) {
							magician.doSkill();
						} else if (thief != null) {
							thief.doSkill();
						} else {
							System.out.println("먼저 캐릭터를 생성해주세요");
						}
						break;
					case 0:
						System.out.println("사용을 종료합니다.");
						System.exit(0);
					default:
						System.out.println("메뉴에 있는 번호를 선택해주세요");
				}
			}
		}
	}
  • 위와 같이 다형성을 전혀 고려하지 않고 만든 소스 코드를 refactoring해보자.
  • skill은 Character만 가지는 게 아니라 Boss도 가질 수 있으니 interface로 빼준다.
public interface Skill {
	void doSkill();
	void doBasicAttack();
}
  • 아래와 같은 형태로 Skill을 전부 impl하게 된다.
public class Novice implements Skill {
	//
	//
	//
	@Override
	public void doSkill() {
		System.out.println("아직 스킬이 없습니다");
	}

	@Override
	public void doBasicAttack() {
		System.out.println("기본공격");
		
	}
};
.
.
//아래는 필요하다면...
public class BossMonster implements Skill {

};
  • 우선 힘, 민, 지의 stat은 이것이 stat임을 확실하게 보여주기 위해 class로 만든다.
  • 이것이 강타입인 java class의 장점이다. class 이름을 통해 어떤 역할을 하는 지 추측할 수 있다.
public class Stat {
	int him;
	int min;
	int ji;

}
  • stat의 총합이 반드시 15가 되어야 하는 조건이 있다.
  • stat은 random 값이기에 random을 불러주고, do - while 문으로 15 이상이 될 떄까지 반복시킨다.
public class Stat {
	int him;
	int min;
	int ji;
	
	Random random = new Random();
	
	public Stat() {
		do {
			this.him = random.nextInt(7) + 1;
			this.min = random.nextInt(7)+ 1;
			this.ji = random.nextInt(7)+ 1;
		} while (him + min + ji < 15);
	}
}
  • 그런데 Knight, Magician, Thief, Novice 모두 Character다
  • Character로서의 stat, name, profession을 가진다
  • 따라서 공통 부모가 될 class를 만들어준다
  • Character를 생성할 떄, 실제로는 Character가 아닌 Novice, Knight, Magician, Thief class로 instance를 만들게 된다
  • Character는 abstract class로 만들어주는게 좋다
    • concrete class인 경우 skill(), attack()을 구현하지 않으면 compile error가 난다
    • 하지만 Character 자체로는 skill과 attack을 특성으로 갖는 것이 아니기에 내용이 없는 method를 구현해야 한다
    • abstract class로 만들어주면 이를 회피하여 구상 class에 구현을 넘길 수 있다
abstract public class Character implements Skill {
	String name;
	String profession;
	Stat stat;
	
	/**
	 * @param name
	 */
	public Character(String name) {
		this.name = name;
		this.stat = new Stat();
	}
	
	public Character() {
		
	}

	@Override
	public String toString() {
		return "[ID: " + this.name 
				+"(" + this.profession + "), stat: "
				+ "힘(" + this.stat.him + "), "
				+ "민(" + this.stat.min + "), "
				+ "지(" + this.stat.ji + ")]";
	}
}
  • 이젠 abstract class에 전부 의존하면 되므로, concrete class에 implements Skill도 전부 없앤다.
  • 없애도 Character abstract class가 Skill을 implements하기 때문에 구현 책임은 concrete class인 Novice에 있다.
  • 따라서 concrete class 단에 굳이 implements Skill을 쓸 필요가 없다.
public class Novice extends Character {
	
	public Novice(String name) {
		super(name);
		this.profession = "초보자";
	}

	@Override
	public void doSkill() {
		System.out.println("아직 스킬이 없습니다");
	}

	@Override
	public void doBasicAttack() {
		System.out.println("기본공격");
		
	}
	
}
.
.
  • 위처럼 abstract class와 interface를 활용하여 다형성을 구현할 수 있다.
  • 이는 util class를 만들 때 특히 도움이 된다.
  • 만약, 부모 class 없이 모두 개별 concrete class로 만들었다면 해당 정보를 개별 method로 만들어서 써야할 것이다.
  • 결국 같은 코드나 다름없는데, 노가다식의 overloading을 하게 되어버린다.
public static void printStatInfo(Novice novice) {
		System.out.println(	
				"부여된 스탯 정보: "
				+ "힘[" + novice.stat.him + "], "
				+ "민[" + novice.stat.min + "], "
				+ "지[" + novice.stat.ji + "]"
		);
	}

public static void printStatInfo(Knight knight) {
	System.out.println(	
			"부여된 스탯 정보: "
			+ "힘[" + knight.stat.him + "], "
			+ "민[" + knight.stat.min + "], "
			+ "지[" + knight.stat.ji + "]"
	);
}

public static void printStatInfo(Magician magician) {
	System.out.println(	
			"부여된 스탯 정보: "
			+ "힘[" + novice.stat.him + "], "
			+ "민[" + novice.stat.min + "], "
			+ "지[" + novice.stat.ji + "]"
	);
}

public static void printStatInfo(Thief thief) {
	System.out.println(	
			"부여된 스탯 정보: "
			+ "힘[" + thief.stat.him + "], "
			+ "민[" + thief.stat.min + "], "
			+ "지[" + thief.stat.ji + "]"
	);
}
  • 하지만 다형성을 잘 생각해서 abstract class로 만들었기에 아래와 같이 간단하게 해주면 된다.
  • 굳이 Novice, Knight, Thief, Magician으로 parameter를 바꿔가며 overloading을 할 필요가 없다.
  • Character로 parameter를 받아주면 그만이다.
public static void printStatInfo(Character character) {
		System.out.println(	
				"부여된 스탯 정보: "
				+ "힘[" + character.stat.him + "], "
				+ "민[" + character.stat.min + "], "
				+ "지[" + character.stat.ji + "]"
		);
	}
  • super class가 만들어지면서 Character를 가져오면 되게끔 바뀐다
public Character(Character character) {
	this.name = character.name;
	this.stat = character.stat;
}

public Knight(Character character) {
	super(character);
	this.profession = "기사";
}
  • 메뉴 선택 시에도 마찬가지다.
  • 전부 Character 객체로 통일해서 진행하면 된다.
  • 매번 새로운 참조 타입을 담는 변수를 만들어 진행할 필요가 전혀 없다.
Character character = null;
switch(menu) {
	case 1: //생성
		character = new Novice(nickname);
		break;
	case 2: //전직
		character = GameUtil.getProfession(character, professionMenu);
		break;
	case 3: //스킬
		character.doSkill();
		break;
	case 4: //기본공격
		character.doBasicAttack();
		break;
	case 5: //정보출력
		GameUtil.printStatInfo(character);
		break;
}
  • 마찬가지로 character가 전직할 때도 Character class로 parameter를 두어 받으면 그만이다.
public static Character getProfession(Character character, int professionMenu) {
	switch (professionMenu) {
		case 1:
			System.out.println("기사로 전직합니다.");
			character = new Knight(character);
			break;
		case 2:
			System.out.println("도적으로 전직합니다.");
			character = new Thief(character);
			break;
		case 3:
			System.out.println("마법사로 전직합니다.");
			character = new Magician(character);
			break;
		default:
			System.out.println("제대로된 거 쓰셈");
	}
	
	return character;
}
  • 참조 떄문에 어쩔수 없이 여러가지 변수를 만들어, null을 의도적으로 통제할 필요가 없다.
  • 소스코드 컨트롤이 훨씬 편해진다. 괜히 null을 잘 넣었나 뺐나 확인할 필요가 없다.
    • Character class를 참조하는 변수 하나만 만들면 끝이다.
    • toString()도 Character class에 규정해두고 그냥 쓰면 된다. field 값은 어차피 알아서 instance 값을 찾아간다.
    • printStatInfo()도 Character parameter로 쓰면 각 직업 class별로 parameter를 받게 overloading할 필요가 없다.
    • getProfession()도 Character parameter로 쓰면 각 직업 class별로 parameter를 받게 overloading할 필요가 없다.
public class Game {
	
	public static void main(String[] args) {
		Character character = null;
		try (Scanner scan = new Scanner(System.in)) {
			while(true) {
				GameUtil.printMenu();
				int menu = scan.nextInt();
				
				switch (menu) {
					case 1:
						System.out.println("캐릭터를 생성합니다.");
						System.out.print("사용하실 아이디를 입력해주세요.");
						String nickname = scan.next(); //nextLine쓰면 이상하게 씹힘.
						System.out.println("스탯을 부여합니다.");
						while (true) {
							character = new Novice(nickname);
							GameUtil.printStatInfo(character);
							System.out.println("스탯을 다시 받으시겠습니까? (y/n)");
							String choice = scan.next(); //nextLine쓰면 이상하게 씹힘.
							if (choice.equals("N") || choice.equals("n")) {
								break;
							} 
						}
						System.out.println("현재 정보로 저장합니다.");
						break;
					case 2:
						if( character == null) {
							System.out.println("먼저 캐릭터를 생성해주세요");
							break;
						} else {
							System.out.println(character.toString()); 
						}
						break;
					case 3:
						if( character == null) {
							System.out.println("먼저 캐릭터를 생성해주세요");
							break;
						}
						//실제 만들 떄는 전직가능 flag 값을 만들어서 진행해야한다.
						//직업 군 내에서의 전직(전사 내 용기사, 아이언 등) or 직업군( 전사 -> 마법사)
						if (!(character instanceof Novice)) {
							System.out.println("이미 전직이 완료되었습니다.");
							break;
						}
						GameUtil.printProfessionMenu();
						int professionMenu = scan.nextInt();
						character = GameUtil.getProfession(character, professionMenu);
						break;
					case 4:
						if( character == null) {
							System.out.println("먼저 캐릭터를 생성해주세요");
							break;
						}
						character.doBasicAttack();
						break;
					case 5:
						if( character == null) {
							System.out.println("먼저 캐릭터를 생성해주세요");
							break;
						}
						character.doSkill();
						break;
					case 0:
						System.out.println("사용을 종료합니다.");
						System.exit(0);
					default:
						System.out.println("메뉴에 있는 번호를 선택해주세요");
				}
			}
		}
	}

}

클래스 나누기3

  • 위에서 보았듯, 클래스를 나누는 것은 중요하다
  • 인터페이스와 extends를 활용해서 클래스를 나눠보자
class Unit {
	int hitPoint;
	final int MAX_HP;

	Unit(int hp){
		MAX_HP = hp;
	}
}

class GroundUnit extends Unit{
	GroundUnit(int hp){
		Super(hp);
	}
}

class AirUnit extends Unit{
	AirUnit(int hp){
		Super(hp)
	}
}
  • 위의 예시와 같이, 나눠져 있을 때는 별도의 객체를 만들기 편리하다
class Tank extends GroundUnit {
	Tank(){
		Super(150);
		hitPoint = MAX_HP;
	}

	public String toString(){
		Return "Tank";
	}
}

class Dropship extends AirUnit {
	Dropship(){
	super(125);
		hitPoint = MAX_HP;
	}
	public String toString(){
		Return "Dropship";
	}
}

class Marine extends GroundUnit{
	Marine(){
	super(40);
		hitPoint = MAX_HP;
	}

	public String toString(){
		Return "Marine";
	}
}

class SCV extends GroundUnit {
	SCV(){
		Super(60);
	}

	public String toString(){
		Return "SCV";
	}
}
  • 하지만 method에 관해서 class를 타지 않아버리는 경우도 있다
    • 수리를 받을 수 있음 같은 특성 같은 경우, Unit에게 달려 있는 특성이 아니라 수리라는 특성이 별도로 존재한다
  • 그럴 때 extends가 아닌 수리가능이라는 interface를 추가한다
  • 그저 타입을 위한 marker interface에 불과하다
interface Repairable(){}
  • 이제 해당 interface를 구현한 class만 수리가능하게 바뀐다
  • Tank, Dropship, SCV는 수리 가능하고, Marine은 수리 불가능하다
class Tank extends GroundUnit implements Repairable{
	Tank(){
		Super(150);
		hitPoint = MAX_HP;
	}

	Public String toString(){
		Return "Tank";
	}
}

class Dropship extends AirUnit implements Repairable{
	Dropship(){
	super(125);
		hitPoint = MAX_HP;
	}

	Public String toString(){
		Return "Dropship";
	}
}

class Marine extends GroundUnit{
	Marine(){
	super(40);
		hitPoint = MAX_HP;
	}
}
  • SCV가 수리를 해주므로 repair method는 SCV에 둔다
  • repair할 대상은 Repairable interface를 구현한 대상으로 한정한다
  • 다만 여기서 문제가 있는 것은, Repairable이 class가 아니기에, hitPoint나 MAX_HP 등을 갖지 못한다는 점이다.
class SCV extends GroundUnit implements Repairable{
	SCV(){
		Super(60);
	}

	Void repair(Repairable r){
		if (r instanceof Unit) {
			Unit u = (Unit)r;
			while(u.hitPoint ! = u.MAX_HP){
				u.hitPoint++;
			}

			Sysout(u.toString() + "의 수리가 끝났습니다.");
		}
	}
}
  • 자 그럼 이제 test를 해보자
  • marine은 repairable interface를 implements하지 않았으므로 에러가 난다.
class RepairableTest{
	public static void main(String[] args){
		Tank tank = new Tank();
		Dropship dropship = new Dropship();

		Marine marine = new Marine();
		SCV scv = new SCV();
		Scv.repair(tank); //ok
		Scv.repair(dropship); //ok
		Scv.repair(marine); //error
		}
}
  • repair 가능한 것은 기계만이 아니라, 건물도 있다
class Building {
	int hitPoint;
	final int MAX_HP;

	Building(int hp){
		MAX_HP = hp;
	}
}

class Barrack extends Building{
	super(1500);

	public String toString(){
		return "Barrack";
	}
}
  • 따라서 아래같이 building instance일 때의 else문이 추가된다.
  • 사실 instanceof가 활용되는 것은 바람직하지 않은 상황이다.
  • building과 unit으로 나눈 설계가 과연 유의미한지 검토가 필요할 수 있다.
class SCV extends GroundUnit implements Repairable{
	SCV(){
		Super(60);
	}

	Void repair(Repairable r){
		if(r instanceof Unit){
			Unit u = (Unit)r; 
			while(u.hitPoint ! = u.MAX_HP){
				u.hitPoint++;
			}
			Sysout(u.toString() + "의 수리가 끝났습니다.");
		}else if (r instanceof Building){
			Building b = (Building)r;  
			while(b.hitPoint != b.MAX_HP){
				b.hitPoint++;
			}
			sysout(b.toString() + "의 수리가 끝났습니다.");
		}
	}
}
  • 결국 설계를 바꿔야 한다는 의미다
  • Repairable의 marker interface가 아니라 실제로 method들을 만든다
  • u.hitPoint나 u.MAX_HP같은 field는 interface로서는 가질 수 없으므로, 아래처럼 3개의 method로 만든다
    • getHitPoint
    • getMaxHP
    • setHitPoint
  • 실제 가져오는 구현은 각 class에서 담당한다.
  • 중복된 소스코드가 만들어지긴 하지만, type casting을 하지 않아도 되어 더 바람직한 구조다
interface Repairable {
    int getHitPoint();
    int getMaxHP();
    void setHitPoint(int hp);
}

// 구체 클래스에서 인터페이스 메서드 구현
class Tank extends GroundUnit implements Repairable {
    Tank() { super(150); hitPoint = MAX_HP; }
    
    // 인터페이스 규격 맞추기
    @Override public int getHitPoint() { return this.hitPoint; }
    @Override public int getMaxHP() { return this.MAX_HP; }
    @Override public void setHitPoint(int hp) { this.hitPoint = hp; }
    @Override public String toString() { return "Tank"; }
}

class Barrack extends Building implements Repairable {
    Barrack() { super(1500); hitPoint = MAX_HP; }

    @Override public int getHitPoint() { return this.hitPoint; }
    @Override public int getMaxHP() { return this.MAX_HP; }
    @Override public void setHitPoint(int hp) { this.hitPoint = hp; }
    @Override public String toString() { return "Barrack"; }
}


void repair(Repairable r) {
    while (r.getHitPoint() < r.getMaxHP()) {
        r.setHitPoint(r.getHitPoint() + 1);
    }
    System.out.println(r.toString() + "의 수리가 끝났습니다.");
}

클래스나누기4

  • 모든 것을 Menu라는 클래스가 담당하고 있는 경우, clicked method가 매우 무거워진다.
public class Application implements OnClickListener {

    private Menu menu1 = new Menu("menu");
    private Menu menu2 = new Menu("menu");
    private Button button1 = new Menu("button1");
    
    private String currentMenu = null;

    public Application() {
        menu1.setOnClickListener(this);
        menu2.setOnClickListener(this);
        button1.setOnClickListener(this);
        button2.setonClickListener(this);
    }

    public void clicked(Component eventSource) {
        if ("menu1".equals(eventSource.getId())) {
            changeUIToMenu1();
        } else if ("menu2".equals(eventSource.getId())) {
            changeUIToMenu2();
        } else if ("button1".equals(eventSource.getId())) {
            if (currentMenu == null) {
                return;
            }
            if ("menu1".equals(currentMenu)) {
                processButton1WhenMenu1();
            } else if ("menu2".equals(currentMenu)) {
                processButton1WhenMenu2();
            }
        } else if (eventSource.getId().equals("button2")) {
            if (currentMenu == null) {
                return;
            }
            if (currentMenu.equals("menu1")) {
                processButton1WhenMenu1();
            } else if (currentMenu.equals("menu2")) {
                processButton1WhenMenu2();
            } 
        }
    }

    private void changeUIToMenu1() {
        currentMenu = "menu1";
        sysout("메뉴1 화면으로 전환");
    }

    private void changeUIToMenu2() {
        currentMenu = "menu2";
        sysout("메뉴2 화면으로 전환");
    }

    private void processButton1WhenMenu1() {
        sysout("메뉴1 화면의 버튼1 처리")
    }

    private void processButton1WhenMenu2() {
        sysout("메뉴2 화면의 버튼1 처리")
    }

    private void processButton2WhenMenu1() {
        sysout("메뉴1 화면의 버튼2 처리")
    }

    private void processButton2WhenMenu2() {
        sysout("메뉴2 화면의 버튼2 처리")
    }

}
  • changeUI나 processButton등을 전부 Application class 내부에서 만들지 말고, 클래스를 나눠준다
public interface ScreenUI {
    public void show();
    public void handleButton1Click();
}

public class Menu1ScreenUI implements ScreenUI {
    public void show() {
        sysout("메뉴1 화면으로 전환");
    }
    public void handleButton1Click() {
        sysout("메뉴1 화면의 버튼1 처리");
    }
}

public class Menu2ScreenUI implements ScreenUI {
    public void show() {
        sysout("메뉴2 화면으로 전환");
    }
    public void handleButton1Click() {
        sysout("메뉴2 화면의 버튼1 처리");
    }
}

public class Application implements OnClickListener {

    private Menu menu1 = new Menu("menu1");
    private Menu menu2 = new Menu("menu2");
    private Button button1 = new Button("button1");

    private ScreenUI currentScreen = null;

    public Application() {
        menu1.setOnClickListener(this);
        menu2.setOnClickListener(this);
        button1.setOnClickListener(this);
    }

    public void clicked(Component eventSource) {
        String sourceId = eventSource.getId();
        if ("menu1".equals(sourceId)) {
            currentScreen = new Menu1ScreenUI();
            currentScreen.show();
        } else if ("menu2".equals(sourceId)) {
            currentSCreen = new Menu2SCreenUI();
            currentScreen.show();
        } else if ("button1".equals(sourceId)) {
            if(currentScreen == null) {
                return;
            }
            currentScreen.handleButton1Click();
        }
    }
}
  • 버튼 클릭 처리 코드와 메뉴 클릭 처리 코드의 목적이 다르므로 분리한다.
public interface ScreenUI {
    public void show();
    public void handleButton1Click();
}

public class Menu1ScreenUI implements ScreenUI {
    public void show() {
        sysout("메뉴1 화면으로 전환");
    }
    public void handleButton1Click() {
        sysout("메뉴1 화면의 버튼1 처리");
    }
}

public class Menu2ScreenUI implements ScreenUI {
    public void show() {
        sysout("메뉴2 화면으로 전환");
    }
    public void handleButton1Click() {
        sysout("메뉴2 화면의 버튼1 처리");
    }
}

public class Application {

    private Menu menu1 = new Menu("menu1");
    private Menu menu2 = new Menu("menu2");
    private Button button1 = new Button("button1");

    private ScreenUI currentScreen = null;
    private OnClickListener menuListener = null;
    private OnClickListener buttonListener = null;

    public Application() {
        menu1.setOnClickListener(this);
        menu2.setOnClickListener(this);
        button1.setOnClickListener(this);
    }

    menuListener = new OnClickListener() {
        public void clicked(Component eventSource) {
            String SourceId = eventSource.getId();
            if("menu1".equals(sourceId)) {
                currentScreen = new Menu1ScreenUI();
            } else if ("menu2".equals(sourceId)) {
                currentScreen = new Menu2ScreenUI();
            }

            currentScreen.show();
        }
    }

    buttonListener = new OnClickListener() {
        public void clicked(Component eventSource) {
            if(currentScreen == null) {
                return;
            }
            String sourceId = eventSource.getId();
            if("button1".equals(sourceId)) {
                currentScreen.handleButton1Click();
            }
        }
    }
}
  • 버튼2가 추가된다.

  • 맨위와 비교하면 정말 간단하게 추가되었음을 알 수 있다.

public interface ScreenUI {
    public void show();
    public void handleButton1Click();
    public void handleButton2Click();
}
public class Menu1ScreenUI implements ScreenUI {
    public void show() {
        sysout("메뉴1 화면으로 전환");
    }
    public void handleButton1Click() {
        sysout("메뉴1 화면의 버튼1 처리");
    }
    public void handleButton2Click() {
        sysout("메뉴1 화면의 버튼2 처리");
    }
}

public class Menu2ScreenUI implements ScreenUI {
    public void show() {
        sysout("메뉴2 화면으로 전환");
    }
    public void handleButton1Click() {
        sysout("메뉴2 화면의 버튼1 처리");
    }
    public void handleButton2Click() {
        sysout("메뉴2 화면의 버튼2 처리");
    }
}
public class Application {

    private Menu menu1 = new Menu("menu1");
    private Menu menu2 = new Menu("menu2");
    private Button button1 = new Button("button1");

    private ScreenUI currentScreen = null;

    public Application() {
        menu1.setOnClickListener(this);
        menu2.setOnClickListener(this);
        button1.setOnClickListener(this);
    }

    private OnClickListener menuListener = new OnClickListener() {
        public void clicked(Component eventSource) {
            String SourceId = eventSource.getId();
            if("menu1".equals(sourceId)) {
                currentScreen = new Menu1ScreenUI();
            } else if ("menu2".equals(sourceId)) {
                currentScreen = new Menu2ScreenUI();
            }

            currentScreen.show();
        }
    }

    private OnClickListener buttonListener = new OnClickListener() {
        public void clicked(Component eventSource) {
            if(currentScreen == null) {
                return;
            }
            String sourceId = eventSource.getId();
            if("button1".equals(sourceId)) {
                currentScreen.handleButton1Click();
            } else if(sourceId.equals("button2")) {
                currentScreen.handleButton2Click();
            }
        }
    }
}
  • 메뉴3가 추가되었다고 해보자.
  • 메뉴3가 추가되도 이제 버튼에 관한 코드는 건들지 않아도 된다.
  • Menu3ScreenUI class만 만들고, Application 생성자에 menu3 관련 clickListener를 달아준다.
public interface ScreenUI {
    public void show();
    public void handleButton1Click();
    public void handleButton2Click();
}
public class Menu1ScreenUI implements ScreenUI {
    public void show() {
        sysout("메뉴1 화면으로 전환");
    }
    public void handleButton1Click() {
        sysout("메뉴1 화면의 버튼1 처리");
    }
    public void handleButton2Click() {
        sysout("메뉴1 화면의 버튼2 처리");
    }
}

public class Menu2ScreenUI implements ScreenUI {
    public void show() {
        sysout("메뉴2 화면으로 전환");
    }
    public void handleButton1Click() {
        sysout("메뉴2 화면의 버튼1 처리");
    }
    public void handleButton2Click() {
        sysout("메뉴2 화면의 버튼2 처리");
    }
}

public class Menu3ScreenUI implements ScreenUI {
    public void show() {
        sysout("메뉴3 화면으로 전환");
    }
    public void handleButton1Click() {
        sysout("메뉴3 화면의 버튼1 처리");
    }
    public void handleButton2Click() {
        sysout("메뉴3 화면의 버튼2 처리");
    }
}
public class Application {

    private Menu menu1 = new Menu("menu1");
    private Menu menu2 = new Menu("menu2");
    private Button button1 = new Button("button1");

    private ScreenUI currentScreen = null;

    public Application() {
        menu1.setOnClickListener(this);
        menu2.setOnClickListener(this);
        menu3.setOnClickListener(this);
        button1.setOnClickListener(this);
    }

    private OnClickListener menuListener = new OnClickListener() {
        public void clicked(Component eventSource) {
            String SourceId = eventSource.getId();
            if("menu1".equals(sourceId)) {
                currentScreen = new Menu1ScreenUI();
            } else if ("menu2".equals(sourceId)) {
                currentScreen = new Menu2ScreenUI();
            } else if (eventSource.getId().equals("menu3")) {
                currentScreen = new Menu3ScreenUI();
            }

            currentScreen.show();
        }
    }

    private OnClickListener buttonListener = new OnClickListener() {
        public void clicked(Component eventSource) {
            if(currentScreen == null) {
                return;
            }
            String sourceId = eventSource.getId();
            if("button1".equals(sourceId)) {
                currentScreen.handleButton1Click();
            } else if(sourceId.equals("button2")) {
                currentScreen.handleButton2Click();
            }
        }
    }
}

클래스나누기5

  • excel을 만들 때는 아래와 같이 excel library를 활용하는 경우가 흔하다.
  • 그 때 excel의 기본 font, style 등은 정해져 있지만, 몇 행 몇 열인지는 domain마다 정해야 하는 경우가 있다.
  • 그럴때 abstract class를 활용한다.
    • createBody만 concrete class에서 구현해준다.
    • createBody를 구현하는 데 필요한 것들만 protected access modifier를 붙여준다.
    • 나머지 private method는 전부 font, color, sheet, header, bodycellstyle 등 공통 요소를 만드는 것들이다.
      • 해당 private method들은 모두 abstract class인 CreateExcel 생성자에서만 쓰인다.
public abstract class CreateExcel<T> {

	/* 상수 START */
	private static final String SHEET_NAME = "엑셀다운로드";
	private static final String FONT_NAME = "Malgun Gothic";
	private static final short FONT_SIZE = 9;
	private static final XSSFColor FONT_COLOR = ExcelUtils.rgbToXSSFColor(127, 127, 127);
	private static final XSSFColor HEADER_BACKGROUND_COLOR = ExcelUtils.rgbToXSSFColor(231, 230, 230);
	private static final XSSFColor HEADER_BORDER_COLOR = ExcelUtils.rgbToXSSFColor(191, 191, 191);
	private static final XSSFColor BODY_BORDER_COLOR = ExcelUtils.rgbToXSSFColor(0, 0, 0);
	private static final HorizontalAlignment HEADER_HORIZONTAL_ALIGNMENT = HorizontalAlignment.CENTER;
	private static final VerticalAlignment HEADER_VERTICAL_ALIGNMENT = VerticalAlignment.CENTER;
	private static final VerticalAlignment BODY_VERTICAL_ALIGNMENT = VerticalAlignment.CENTER;
	private static final BorderStyle BORDER_STYLE = BorderStyle.THIN;

	private final ExcelColumn[] COLUMN_LIST;
	/* 상수 END */

	/* 파일 준비 START */
	private final XSSFWorkbook workbook;
	private final XSSFSheet sheet;

	private XSSFFont headerFont;
	private XSSFFont bodyFont;
	private XSSFCellStyle headerCellStyle;
	private XSSFCellStyle bodyCellStyleCenter;
	private XSSFCellStyle bodyCellStyleLeft;
	private XSSFCellStyle emptyCellStyle;

	private int rowNo = 0;
	private int colNo = 0;
	private XSSFRow xssfRow;

	protected PMSCreateExcel(ExcelColumn[] columnList) {
		this.COLUMN_LIST = columnList;

		this.workbook = new XSSFWorkbook();
		this.sheet = createSheet();
		this.headerFont = createFont(true);
		this.bodyFont = createFont(false);
		this.headerCellStyle = createHeaderCellStyle();
		this.bodyCellStyleCenter = createBodyCellStyle(HorizontalAlignment.CENTER);
		this.bodyCellStyleLeft = createBodyCellStyle(HorizontalAlignment.LEFT);
		this.emptyCellStyle = workbook.createCellStyle();
	}

	private XSSFSheet createSheet() {
		XSSFSheet sheet = workbook.createSheet(SHEET_NAME);

		for (int i = 0; i < COLUMN_LIST.length; i++) {
			int width = COLUMN_LIST[i].getWidth();
			ExcelUtils.excelSetColumnWidth(sheet, i, width);
		}

		return sheet;
	}

	private XSSFFont createFont(boolean isBold) {
		XSSFFont font = workbook.createFont();
		font.setFontName(FONT_NAME);
		font.setFontHeightInPoints(FONT_SIZE);
		font.setColor(FONT_COLOR);
		font.setBold(isBold);

		return font;
	}

	private XSSFCellStyle createCellStyle() {
		return workbook.createCellStyle();
	}

	private XSSFCellStyle createHeaderCellStyle() {
		XSSFCellStyle headerCellStyle = createCellStyle();
		headerCellStyle.setFont(headerFont);
		headerCellStyle.setAlignment(HEADER_HORIZONTAL_ALIGNMENT);
		headerCellStyle.setVerticalAlignment(HEADER_VERTICAL_ALIGNMENT);
		headerCellStyle.setFillForegroundColor(HEADER_BACKGROUND_COLOR);
		headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
		headerCellStyle.setBorderColor(BorderSide.LEFT, HEADER_BORDER_COLOR);
		headerCellStyle.setBorderColor(BorderSide.RIGHT, HEADER_BORDER_COLOR);
		headerCellStyle.setBorderLeft(BORDER_STYLE);
		headerCellStyle.setBorderRight(BORDER_STYLE);

		return headerCellStyle;
	}

	private XSSFCellStyle createBodyCellStyle(HorizontalAlignment horizontalAlignment) {
		XSSFCellStyle bodyCellStyle = createCellStyle();
		bodyCellStyle.setFont(bodyFont);
		bodyCellStyle.setAlignment(horizontalAlignment);
		bodyCellStyle.setVerticalAlignment(BODY_VERTICAL_ALIGNMENT);
		bodyCellStyle.setBorderColor(BorderSide.LEFT, BODY_BORDER_COLOR);
		bodyCellStyle.setBorderColor(BorderSide.RIGHT, BODY_BORDER_COLOR);
		bodyCellStyle.setBorderColor(BorderSide.TOP, BODY_BORDER_COLOR);
		bodyCellStyle.setBorderColor(BorderSide.BOTTOM, BODY_BORDER_COLOR);
		bodyCellStyle.setBorderLeft(BORDER_STYLE);
		bodyCellStyle.setBorderRight(BORDER_STYLE);
		bodyCellStyle.setBorderTop(BORDER_STYLE);
		bodyCellStyle.setBorderBottom(BORDER_STYLE);

		return bodyCellStyle;
	}
	/* 파일 준비 END */

	/* 파일 데이터 입력 START */
	/** 파일 생성 **/
	public Workbook create(List<T> dataList) {
		createHeader();
		createBody(dataList);

		return workbook;
	}

	/** 파일 생성(헤더) **/
	private void createHeader() {
		newRow();

		for (ExcelColumn column : COLUMN_LIST) {
			String headerText = column.getName();
			createHeaderCell(headerText);
		}

		for (int i = 0; i < 3; i++) {
			createEmptyCell();
		}
	}

	/** 파일 생성(바디) **/
	protected abstract void createBody(List<T> assetList);
	/* 파일 데이터 입력 END */

	/* 유틸리티 함수 START */
	protected void newRow() {
		xssfRow = sheet.createRow(rowNo++);
		colNo = 0;
	}

	private void createCell(String value, XSSFCellStyle cellStyle) {
		ExcelUtils.excelAddCell(value, cellStyle, colNo++, xssfRow);
	}

	private void createHeaderCell(String value) {
		ExcelUtils.createCell(value, headerCellStyle);
	}

	protected void createBodyCellCenter(String value) {
		ExcelUtils.createCell(value, bodyCellStyleCenter);
	}

	protected void createBodyCellLeft(String value) {
		ExcelUtils.createCell(value, bodyCellStyleLeft);
	}

	private void createEmptyCell() {
		ExcelUtils.createCell("", emptyCellStyle);
	}

	protected String dateText(Date date) {
		return date == null ? "" : DateUtil.dateToString(date);
	}
	/* 유틸리티 함수 END */

}
  • 실제 자산신청서 양식은 아래와 같이 파일을 생성한다.
public class RentApplicationDetailCreateExcel extends CreateExcel<RentAssetDTO> {

	private static final ExcelColumn[] COLUMN_LIST = { col("자산 코드", 10), col("자산 코드(구)", 11), col("품목", 12),
			col("모델명", 35), col("모델번호", 15), col("일련번호", 20), col("추가 정보", 16), col("사용자", 13), col("대여일", 13),
			col("대여장소", 13), col("반납일", 13), col("반납장소", 13), col("신청상태", 13), col("비고", 11) };

	public RentApplicationDetailCreateExcel() {
		super(COLUMN_LIST);
	}

	@override
	protected void createBody(List<RentAssetDTO> rentAssetList) {
		for (RentAssetDTO asset : rentAssetList) {

			log.info("asset code : " + asset.getAssetCode());

			newRow();

			createBodyCellCenter(asset.getAssetCode());
			createBodyCellCenter(asset.getAssetCodeOld());
			createBodyCellCenter(AssetItemCodeEnum.fromCommonCode(asset.getAssetItemCode()).getAssetCodeNm());
			createBodyCellLeft(asset.getModelName());
			createBodyCellLeft(asset.getModelNo());
			createBodyCellLeft(asset.getSerialNo());
			createBodyCellLeft(asset.getAssetRemark());
			createBodyCellLeft(asset.getUser().getUserNm());
			createBodyCellCenter(asset.getRentDate());

			createBodyCellLeft(asset.getRentPlace());
			createBodyCellCenter(asset.getReturnDate());
			createBodyCellLeft(asset.getReturnPlace());

			String applyStateCode = asset.getApplyStateCode();
			String applyState = "";
			if (applyStateCode != null) {
				applyState = ApplState.fromCommonCode(applyStateCode).getDisplayName();
			}

			createBodyCellCenter(applyState);
			createBodyCellLeft(asset.getRemark());

		}
	}

}
  • 자산신청이 아닌 자산 그 자체 관리의 경우는 양식이 또 다르다.
  • 따라서 createBody가 다른 식으로 구현된다.
public class AssetServiceCreateExcelAllAssets extends CreateExcel<Asset> {

	private static final ExcelColumn[] COLUMN_LIST = { col("자산 코드", 10), col("자산 코드(구)", 10), col("품목", 11),
			col("제조사", 7), col("모델명", 34), col("모델번호", 14), col("일련번호", 19), col("OS버전", 25), col("CPU", 23),
			col("RAM", 12), col("SSD 용량", 11), col("그래픽", 18), col("제조시기", 7), col("화면크기(인치)", 20), col("최대 해상도", 20),
			col("추가 정보", 8), col("자산위치", 18), col("자산상태", 7), col("대여상태", 12), col("사용자", 8), col("대여일", 10),
			col("반납일", 10), col("신청상태", 8), col("등록자", 7), col("등록일", 9), col("수정자", 7), col("수정일", 9) };

	public AssetServiceCreateExcelAllAssets() {
		super(COLUMN_LIST);
	}

	@override
	protected void createBody(List<Asset> assetList) {
		for (Asset asset : assetList) {
			newRow();

			createBodyCellCenter(asset.getAssetCode());
			createBodyCellCenter(asset.getAssetCodeOld());
			createBodyCellCenter(asset.getAssetItem().getDisplayName());
			createBodyCellCenter(asset.getAssetMnfcCo());
			createBodyCellLeft(asset.getAssetModelNm());
			createBodyCellLeft(asset.getAssetModelNo());
			createBodyCellLeft(asset.getAssetSerNo());
			createBodyCellLeft(asset.getAssetOSVer());
			createBodyCellLeft(asset.getAssetCPU());
			createBodyCellLeft(asset.getAssetRAM());
			createBodyCellLeft(asset.getAssetSSD());
			createBodyCellLeft(asset.getAssetGraphic());
			createBodyCellLeft(asset.getAssetMnfcDt());
			createBodyCellLeft(asset.getAssetScreenSize());
			createBodyCellLeft(asset.getAssetMaxRes());
			createBodyCellLeft(asset.getAssetRemark());
			createBodyCellLeft(asset.getAssetCurPlace());
			createBodyCellCenter(asset.getAssetState().getDisplayName());

			AssetRentState assetRentState = asset.getAssetRentState();
			createBodyCellCenter(assetRentState.getRentState().getDisplayName());

			String userNm = "";
			String rentDtStr = "";
			String returnDtStr = "";
			String applStateStr = "";

			RentState rentState = assetRentState.getRentState();
			if (rentState == RentState.RENT_APPLY || rentState == RentState.RENTED || rentState == RentState.RETURN_APPLY) {
				User lastUser = assetRentState.getApplAsset().getUserSeq();

				userNm = lastUser.getUserNm();
				rentDtStr = dateText(assetRentState.getApplAsset().getRentDt());
				returnDtStr = dateText(assetRentState.getApplAsset().getReturnDt());

				applStateStr = rentState.getDisplayName();
			}

			createBodyCellLeft(userNm);
			createBodyCellCenter(rentDtStr);
			createBodyCellCenter(returnDtStr);
			createBodyCellCenter(applStateStr);

			createBodyCellLeft(asset.getRegUser().getUserNm());
			createBodyCellCenter(dateText(asset.getRegDt()));

			User updateUser = asset.getUpdateUser();
			String updateUserStr = updateUser == null ? "" : updateUser.getUserNm();
			createBodyCellLeft(updateUserStr);
			createBodyCellCenter(dateText(asset.getUpdateDt()));
		}
	}

}

클래스나누기6

public class FlowController {
    private boolean useFile;

    public FlowController(boolean useFile) {
        this.useFile = useFile;
    }

    public void process() {
        byte[] data = null;
        if (useFile) {
            FileDataReader fileReader = new FileDataReader();
            data = fileReader.read();
        } else {
            SocketDataReader socketReader = new SocketDataReader();
            data = socketReader.read();
        }

        Encryptor encryptor = new Encryptor();
        byte[] encryptedData = encryptor.encrypt(data);

        FileDataWriter writer = new FileDataWriter();
        writer.write(encryptedData);
    }
}
  • 아래와 같이 interface를 사용해 책임을 나누자.
public interface ByteSource {
    public byte[] read();
}

public class FileDataReader implements ByteSource {
    public byte[] read() {

    }

    public class SocketDataReader implements ByteSource {

    }
}

public class FlowController {
    private boolean useFile;

    public FlowController(boolean useFile) {
        this.useFile = useFile;
    }

    public void process() {
       ByteSource source = null;
        if (useFile) {
          source = new FileDateReader();
        } else {
           source = new SocketDataReader();
        }

        byte[] data = source.read();

        Encryptor encryptor = new Encryptor();
        byte[] encryptedData = encryptor.encrypt(data);

        FileDataWriter writer = new FileDataWriter();
        writer.write(encryptedData);
    }
}
  • 그리고 ByteSource 또한 종류가 변경되도 FlowController가 바뀌지 않게 만든다.
  • 그 방법은 class 안에 분류 로직을 만드는 것이다.
  • 분류기는 매번 instance를 만들 필요가 없으니 singleton으로 만든다.
public class ByteSourceFactory {
    public ByteSource create() {
        if (useFile()) {
            return new FileDataReader();
        } else {
            return new SocketDataReader();
        }
    }

    private boolean useFile() {
        String useFileVal = System.getProperty("useFile");
        
        return useFileVal != null && Boolean.valueOf(useFileVal);
    }

    private static ByteSourceFactory instance = new ByteSourceFactory();
    public static ByteSourceFactory getInstance() {
        return instance;
    }
    private ByteSourceFactory() {}
}

public class FlowController {
    private boolean useFile;

    public FlowController(boolean useFile) {
        this.useFile = useFile;
    }

    public void process() {
        ByteSource source = ByteSourceFactory.getInstance().create();
        byte[] data = source.read();

        Encryptor encryptor = new Encryptor();
        byte[] encryptedData = encryptor.encrypt(data);

        FileDataWriter writer = new FileDataWriter();
        writer.write(encryptedData);
    }
}
  • FlowController를 위와 같이 바꾸면 새로운 요청 사항이 들어와도, FlowController를 건들필요가 없다.
  • HTTPs를 이용해서 데이터를 읽어오려면 ByteSourceFactory의 create에만 관련 내용을 넣어주면 된다.
  • 이전 코드를 보면 데이터를 읽어오는 객체를 생성하는 책임, 흐름을 제어하는 책임이 한 객체에 몰아져 있었음을 알 수 있다.
public void process() {
    //데이터 읽기 객체 직접 생성
    FileDataReader reader = new FileDataReader();
    //흐름 제어: 1. 읽기
    byte[] data = reader.read();

    //흐름 제어: 2. 암호화
    Encryptor encryptor = new Encryptor();
    byte[] encryptedData = encryptor.encrypt(data);
    
    //흐름 제어: 3. 쓰기
    FileData Writer writer = new FileDataWriter();
    writer.write(encryptedData);
}
  • 진행한 두 번의 추상화는 다음과 같다.
바이트 데이터 읽기: ByteSource interface 도출
ByteSource 객체 생성하기: ByteSourceFactory 도출
  • 이러한 추상화를 통해 상위 수준의 controller 로직을 건들지 않고 그대로 놔둘 수 있었다.
  • 이처럼 상위 수준(controller) logic은 바뀌지 않게 확장가능한 설계를 짜는 것이 중요하다.
  • 그러한 추상화를 위해서 바로 중요한 게 interface를 활용하는 것이다.
  • interface는 자바 내부의 interface가 아니라 객체의 스펙, 규격서라는 개념에서의 interface다.
program to interface(인터페이스에 대고 프로그래밍하기)  
다만 interface를 쓰면 복잡해지므로 변화가능성이 높은 곳에만 활용해야 한다.
  • 인터페이스는 인터페이스 사용자 입장에서 만들어야 한다.
  • FileDateReader와 SocketFileDataReader class를 모두 아우르는 naming이 필요하다.
  • 더 나아가 본질을 포착해 naming을 해야한다. 어쨌든 데이터를 읽어온다는 의미에서 ByteSource라고 지어주는 게 더 좋다.
  • FileDateReaderInterface는 바람직하지 않은 naming이다.
  • 객체를 나누면 좋은 다른 점은 test가 가능해진다는 점이다.
public class FlowController {
    public void process() {
        FileDataReader reader = new FileDataReader(); //구현못했으면 read() test 불가.. interface를 implements한 class 만들어 대리 test도 불가. IF가 아예 없음. concrete class만 존재
        byte[] data = reader.read();
    }
}

public void testProcess() {
    FlowController fc = new FlowController();
    fc.process();
}
  • 아래와 같이 mock 객체를 활용할 수 있다.
  • mock 객체를 활용하면 FileDataReader의 read가 구현이 끝나지 않았어도 test가 가능하다.
public class FlowController {
    private ByteSource byteSource;

    public FlowController(ByteSource byteSource) {
        this.byteSource = byteSource;
    }

    public void process() {
        byte[] data = byteSource.read();
    }
}

public void testProcess() {
    ByteSource mockSource = new MockByteSource();
    FlowController fc = new FlowController(mockSource);
    fc.process();
}

class MockByteSource implements ByteSource {
    public byte[] read() {
        byte[] data = new byte[128];

        return data;
    }
}

캡슐화

  • 회원 만료 처리를 하는 코드가 아래와 같다고 해보자.
@Getter
public class Member {
    private Date expiryDate;
    private boolean male;
}
.
.
.

if (member.getExpiryDate() != null &&
        member.getExpiryDate().getDate() < System.currentTimeMills()) {
            //만료됐을 때 처리
}
  • 그런데 서비스를 운영하다가 여성회원은 만료 기간이 지나도 30일 간은 서비스를 사용하게 정책이 변경되었다.
  • 그럼 만료가 사용되는 코드를 전부 아래와 같이수정해줘야 한다.
long day30 = 1000 * 60 * 60 * 24 * 30;
if( (member.isMale && member.getExpiryDate ! = null && member.getExpiryDate().getDate() < System.currentTimeMills()) ||
    (!member.isMale() && member.getExpiryDate != null && member.getExpiryDate().getDate() < System.currentTimeMills() - day30)) {

    }
  • 만료 처리 로직을 캡슐화해준다면 전부 수정하지 않아도 된다.
  • 해당 class 내의 로직만 수정해주면 나머지는 알아서 적용된다.
  • 아래와 같이 기존 만료 로직이 있다고 해보자.
public class Member {
    private Date expiryDate;
    private boolean male;

    public boolean isExpired() {
        return expiryDate != null &&
                expiryDate.getDate() < System.currentTimeMills();
    }
}

if(member.isExpired) {

}
  • 기존 로직을 아래와 같이 바꿨다.
  • 클래스에 캡슐화해놨기 때문에 서비스의 모든 로직을 수정할 필요가 없다.
public class Member {
    private static final long DAY30 =  1000 * 60 * 60 * 24 * 30;
    private Date expiryDate;
    private boolean male;

    public boolean isExpired() {
        if (male) {
            return expiryDate != null &&
                expiryDate.getDate() < System.currentTimeMills();
        }

        return expiryDate != null &&
                expiryDate.getDate() < System.currentTimeMills() - DAY30;
    }
}

if(member.isExpired()) {

}
  • 이와 같이 캡슐화의 유명한 원칙으로 두 가지가 있다.
    • 객체를 생성하고 매서드를 호출했으면 기능이 알아서 실행되게 하자는 것이다.
    • 의존을 가진 객체의 method만 호출하자는 것이다.
public void processSome(Member member) {
    if (member.getDate().getTime() < ..) {// getDate에서 또 getTime을 부르면 데메테르 위반

    }
}
  • 데메테르 법칙에 관한 유명한 코드는 아래와 같다.
  • 아래 코드로 돈을 받을 수 있지만, 이를 실제 현실로 옮기면 다음과 같은 절차다.
  • 지갑을 받는 게 아니라 고객이 돈을 지불하게 바꿔야 한다.
고객님 지갑주세요
지갑에 돈있는지 볼게요
돈있으니까 돈 빼갈게요
  • 그 여파로 wallet.getTotalMoney()을 보면 customer.getWallet().getTotalMoney()로 2번 호출한 모습이다
  • 거기다 문제는 돈받는 코드의 모든 로직이 바뀌어야 하는데, 서비스가 2개면 2개의 파일이 연동을 받아 버린다
public class Customer {
    private Wallet wallet;

    public Wallet getWallet() {
        return wallet;
    }
}

public class Wallet {
    private int money;
    public int getTotalMoney() {
        return money;
    }
    public void substractMoeny(int debit) {
        moeny -=debit;
    }
}

public serviceA() {
    //돈받는 코드
    int payment = 10000;
    Wallet wallet = customer.getWallet();
    if (wallet.getTotalMoney() >= payment) {
        wallet.substractMoney(payment);
    } else {
        //다음에 요금 받게 처리
    }
}

public serviceB() {
    //돈받는 코드
    int payment = 10000;
    Wallet wallet = customer.getWallet();
    if (wallet.getTotalMoney() >= payment) {
        wallet.substractMoney(payment);
    } else {
        //다음에 요금 받게 처리
    }
}

  • 아래와 같이 class 내부로 캡슐화하면, 객체의 객체의 method를 부르는 일이 없어진다.
  • 또한 지갑에서 돈을 받는 게 주머니에서 돈을 받는 방식으로 구현이 바뀌어도 getPayment()만 바꿔주면 된다.
public class Customer {
    private Waller wallet;

    public int getPayment(int payment) {
        if (wallet == null) {
            throw new NotEnoughMoneyException();
        }
        if (wallet.getTotalMoney() >= payment) {
            wallet.substractMoney(payment);
            
            return payment;
        }

        throw new NotEnoughMoneyException();
    }
}

//돈 받는 코드
int payment = 10000;
try {
    int paidAmount = customer.getPayment(payment);
} catch(NotEnoughMoneyException e) {

}