생성 패턴 Creation Pattern 

 

싱글톤 패턴 Singleton Pattern

객체 인스턴스가 한개만 생성

정보를 보관하고, 공유하고자 하는 클래스의 객체를 한 번의 메모리만 할당하고 그 할당된 메모리에 대해 객체로 관리하는 것이 목적

 

ex) 회사 이름, 주소, 전화번호 등의 정보를 담고있는 Company 클래스의 객체를 하나만 생성해 두면, 서로 다른 클래스들이 Company 객체를 사용하여 회사의 정보 사용 가능

어떤 클래스에서 회사 정보를 수정하더라도 다른 객체에서 Company 객체를 조회하면 그 수정된 정보를 그대로 사용 가능

 

 

public class CompanyInfo {


    private static CompanyInfo companyInfo;
    private  String companyName;
    private  String companyAddress;
    

    private CompanyInfo() {

    }


    public static CompanyInfo getInstance(){
        if(companyInfo == null){
            synchronized (CompanyInfo.class){
                companyInfo = new CompanyInfo();
            }
        }

        return companyInfo;
    }

    public String getCompanyName() {
        return companyName;
    }

    public void setCompanyName(String companyName) {
        this.companyName = companyName;
    }

    public String getCompanyAddress() {
        return companyAddress;
    }

    public void setCompanyAddress(String companyAddress) {
        this.companyAddress = companyAddress;
    }

}

외부에서 CompanyInfo 클래스의 인스턴스를 new 키워드로 생성하지 않고 getInstance 메소드를 통해 접근

프로그램 실행 후 최초 호출이라면 새로운 인스턴스를 생성하고, 이미 생성된 인스턴스가 있다면 해당 인스턴스를 반환

 

public class SetCompanyInfo{

    public void setCompanyName(String newName){
        CompanyInfo companyInfo = CompanyInfo.getInstance();
        companyInfo.setCompanyName(newName);
    }

    public void setCompanyName(String newAddr){
        CompanyInfo companyInfo = CompanyInfo.getInstance();
        companyInfo.setCompanyAddress(newAddr);
    }
}

public class GetCompanyInfo{

    public String getCompanyName(){
        return  CompanyInfo.getInstance().getCompanyName();
    }

    public String getCompanyAddress(){
        return  CompanyInfo.getInstance().getCompanyAddress();
    }
}

SetCompanyInfo 는 회사의 정보를 수정하는 클래스

GetCompanyInfo 는 회사의 정보를 조회하는 클래스

 

각각 다른 클래스임에도 하나의 인스턴스에 접근하여 정보를 수정, 조회하며 정보의 동기화 가능

 

 

 

팩토리 메소드 패턴 Factory Method Pattern

객체를 생성할 때 필요한 인터페이스 구현

어떤 클래스의 인터페이스를 만들 지는 서브 클래스에서 결정

팩토리 메소드 패턴을 사용하면 클래스 인스턴스 만드는 작업은 서브 클래스에서 수행

 

ex) 어떤 상황에서 조건에 따라 객체를 다르게 생성해야 할 경우

사용자의 입력값에 따라 하는 일이 달라지는 경우

 

팩토리 메소드 패턴은 분기에 따른 객체의 생성을 직접 하지 않고 팩토리라는 클래스에 위임하여 팩토리 클래스가 객체 생성하도록 구현하는 방식

말 그대로 객체 찍어내는 공장

 

 

public abstract class Type{
}
public class first extends Type{

    public first() {
        System.out.println("first class create");
    }
}
public class second extends Type{

    public second() {
        System.out.println("second class create");
    }
}
public class third extends Type{

    public third() {
        System.out.println("third class create");
    }
}
public class ClassA{

    public Type createType(String typeName){
        Type newType = null;


        if(type = "first")
            newType = new first();
        else if(type = "second")
            newType = new second();
        else if(type = "third")
            newType = new third();

        return newType;
    }
}
public class Main(){

    public static void main(String args[]){
        ClassA classA = new ClassA();
        classA.createType("first");
        classA.createType("second");
    }
}

 

입력한 문자열에 따라 first, second, third 클래스를 생성하는 ClassA 클래스를 main 에서 활용하는 코드

여기까진 정상적으로 보이나, 문제는 ClassA 뿐만 아니라 다른 객체에서도 first, second, third 클래스 인스턴스를 생성하는 코드를 작성해야 한다면??

 

해당하는 모든 클래스에 if 문이나 switch 문을 사용해서 first, second, third 클래스를 생성해야 함

이는 코드의 중복에 해당하며 객체 사이의 결합도가 매우 강한 구조

결합도가 강하다면, 코드가 길어지고 복잡해질 경우 유지보수가 매우 어려움

 

따라서 객체 인스턴스를 생성해주는 공장, 팩토리를 사용함으로서 이런 중복된 코드의 사용을 막는 것이 팩토리 메소드 패턴

 

public class FactoryClass{

    public Type createType(String typeName){
        Type newType = null;


        if(type = "first")
            newType = new first();
        else if(type = "second")
            newType = new second();
        else if(type = "third")
            newType = new third();

        return newType;
    }
}

기존의 ClassA 에서 수행하던 조건 분기를 FactoryClass 가 수행

 

public class ClassA{

    public Type createType(String typeName){
        FactoryClass factoryClass = new FactoryClass();
        return  factoryClass.createType(typeName);
    }
}

기존의 ClassA 는 이 FactoryClass 를 사용해서 넘겨받은 문자열에 해당하는 객체 인스턴스 생성

ClassA 뿐만 아니라 ClassB, ClassC, ClassD .....  등 같은 동작을 필요로 하는 클래스들도 마찬가지로 FactoryClass 를 사용하여 객체 인스턴스 생성을 위임하면 똑같은 조건문을 무수히 반복해 작성할 필요 X

 

 

 

 

추상팩토리 패턴 Abstract Factory Pattern

 

구상 클래스에 의존하지 않고 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스 제공

구상클래스는 서브 클래스에서 구현

 

팩토리 메소드 패턴과 비슷해 보이지만, 차이점이 존재

 

팩토리 메소드 패턴

조건에 따른 객체 생성을 팩토리에 위임

팩토리 클래스에서 실제 객체 생성

 

추상 팩토리 패턴

서로 관련있는 객체를 묶어서 팩토리 클래스 생성

팩토리를 조건에 따라 생성하도록 다시 팩토리를 만들어서 객체 생성하는 패턴

 

 

추상팩토리 패턴은 팩토리 메소드 패턴을 좀 더 캡슐화 한 방식이라고 보면 됨

 

 

ex)

한 공장에서 특정 Product 를 만들 때 부품 두 개가 필요한 상황

Product A 를 만들기 위해서는 두 부품 모두 A 제조사의 것을 사용해야 하고, Product B 의 경우는 B 제조사의 부품을 사용해야 함

 

이를 팩토리 메소드 패턴으로 구현해보면

public class FirstPartA implements FirstPart{
    public FirstPartA(){
        System.out.println("using first part of A");
    }
}
public class SecondPartA implements SecondPart{
    public SecondPartA(){
        System.out.println("using second part of A");
    }
}
public class FirstPartB implements FirstPart{
    public FirstPartB(){
        System.out.println("using first part of B");
    }
}
public class SecondPartB implements SecondPart{
    public SecondPartB(){
        System.out.println("using second part of B");
    }
}

 

위와 같이 A, B 제조사의 부품 두 개를 생성하는 클래스를 각각 선언

 

public class FirstPartFactory(){

    public FirstPart createFirstPart(String type){

        FirstPart firstPart = null;
        switch(type){
            case "A" :
                firstPart = new FirstPartA();
                break;

            case "B" :
                firstPart = new FirstPartB();
                break;

        }

        return firstPart;
    }
}
public class SecondPartFactory(){

    public SecondPart createSecondPart(String type){

        SecondPart secondPart = null;
        switch(type){
            case "A" :
                secondPart = new SecondPartA();
                break;

            case "B" :
                secondPart = new SecondPartB();
                break;

        }

        return secondPart;
    }
}

 

첫 번째와 두 번째 부품의 Factory 클래스를 각각 선언하여 A, B 제조사 중 어떤 제조사의 부품을 생성할 것인지 선택

 

 

public class ProductFactory(){

    public void createProduct(String type){

        FirstPartFactory firstPartFactory = new FirstPartFactory();
        SecondPartFactory secondPartFactory = new SecondPartFactory();

        firstPartFactory.createFirstPart(type);
        secondPartFactory.createSecondPart(type);
    }
}

Product 를 실제 생성하는 ProductFactory

 

public class Main(){

    public static void main(String args[]){

        ProductFactory productFactory = new ProductFactory();
        productFactory.createProduct("A");
    }
}

원하는 Product 의 제조사를 입력하면 위의 Factory 들을 거쳐 A 혹은 B 제조사의 부품들로만 이루어진 Product 가 생성

 

 

근데 사실, Product A 라면 당연히 A 제조사의 부품을 사용하고, Product B 라면 당연히 B의 제조사의 제품들만 사용하기 때문에 각각 부품을 제조사별로 구분할 필요 없이 Product A 는 A 제조사, B라면 B의 제조사 부품만 사용하도록 생성하면 됨

 

public interface ProductFactory{
    public FirstPart createFirstPart();
    public SecondPart createSecondPart();
}

첫 번째 부품, 두 번째 부품을 생성하는 메소드를 가진 공통 인터페이스를 만들고

 

public class ProductAFactory() implements ProductFactory{

    public FirstPart createFirstPart(){
        return new FirstPartA();
    }

    public SecondPart createSecondPart(){
        return new SecondPartA();
    }
}
public class ProductBFactory() implements ProductFactory{

    public FirstPart createFirstPart(){
        return new FirstPartB();
    }

    public SecondPart createSecondPart(){
        return new SecondPartB();
    }
}

이를 implements 하여 제조사 별 Factory 클래스를 생성

 

public class FactoryOfProductFactory(){
    public void createProduct(String type){
        ProductFactory productFactory = null;

        switch (type){
            case "A" :
                productFactory = new ProductAFactory();
                break;
            case "B":
                productFactory = new ProductBFactory();
                break;
        }

        productFactory.createFirstPart();
        productFactory.createSecondPart();
    }
}

원하는 제조사 별로 조건을 분기하는 기능을 가진 상위 Factory 생성 (ProductFactory 의 Factory)

 

public class Main {
    public static void main(String args[]){
        FactoryOfProductFactory factoryOfProductFactory = new FactoryOfProductFactory();
        factoryOfProductFactory.createProduct("A");

    }
}

이로써 원하는 제조사의 Product 를 생성 가능하게 된다

팩토리 메소드 패턴과 결과는 같지만, 추상 팩토리 패턴은 각각의 제조사별로 Product 제조 과정을 ProductFactory 인터페이스를 통해 개별로 구현이 가능

 

 

그림으로 그려보면, 이러한 방식으로 동작

 

 

- 어떤 제조사의 부품을 선택할 지 결정하는 Factory 클래스가 없어지고, ProductFactory 클래스가 추가

- ProductAFactory, ProductBFactory 클래스는 ProductFactory 인터페이스로 캡슐화

어떤 제조사의 부품을 사용할 것인지 명확하기 때문에 요청에 따른 제조사의 부품 생성 (일관된 객체 생성)

- FactoryOfProductFactory 클래스에서 컴퓨터를 생성하는 createProduct() 메소드 호출

 

 

 

 

 

행동 패턴 Behavioral Pattern

 

템플릿 메소드 패턴 Template Method Pattern

어떤 작업을 처리하는 일부분을 서브클래스로 캡슐화 하여 전체 일을 수행하는 구조는 그대로 두고 특정 단계에서 수행하는 내역을 바꾸는 패턴

 

전체적으로 동일하면서 부분적으로는 다른 구문으로 구성된 메소드의 코드 중복 최소화

 

 

 

AbstractClass

템플릿 메소드를 정의하는 클래스

하위 클래스에 공통 알고리즘 정의, 하위 클래스에서 구현될 기능을 primitive 메소드나 hook 메소드로 정의하는 클래스

 

ConcreteClass

물려받은 primitive 메소드나 hook 메소드를 구현하는 클래스

상위 클래스에 구현된 템플릿 메소드의 일반적인 알고리즘에서 하위클래스에 적합하게 primitive 메소드나 hook 메소드를 재정의하는 클래스

 

 

hook method

부모의 템플릿 메소드의 영향이나 순서를 제오하고 싶을 때 사용되는 메소드 형태

 

 

 

ex)

아이스 아메리카노, 아이스 라떼를 각각 레시피대로 제조한다고 할 때,

 

아이스 아메리카노는

1. 물을 끓인다

2. 끓는 물에 에스프레소를 넣는다

3. 얼음을 넣는다

4. 시럽을 넣는다

 

아이스 라떼는 

1. 물을 끓인다

2. 끓는 물에 에스프레소를 넣는다

3. 얼음을 넣는다

4. 우유를 넣는다

 

 

이를 코드로 표현하면

 

public class IceAmericano {

    public void makeAmericano(){
        boilWater();
        putEspresso();
        putIce();
        putSyrup();
    }

    private void boilWater(){
        System.out.println("boil water");
    }

    private void putEspresso(){
        System.out.println("put espresso");
    }
    private void putIce(){
        System.out.println("put ice");
    }

    private void putSyrup(){
        System.out.println("put syrup");
    }
}
public class IceLatte {

    public void makeLatte(){
        boilWater();
        putEspresso();
        putIce();
        putMilk();
    }

    private void boilWater(){
        System.out.println("boil water");
    }

    private void putEspresso(){
        System.out.println("put espresso");
    }
    private void putIce(){
        System.out.println("put ice");
    }

    private void putMilk(){
        System.out.println("put milk");
    }
}
public class Main(){

    public static void main(String args[]) {

        IceAmericano iceAmericano = new IceAmericano();
        iceAmericano.makeAmericano();
    }
}

 

이렇게 표현 가능

아메리카노와 라떼 사이에 겹치는 공통적으로 겹치는 메소드 확인 가능

 

이를 템플릿 메소드 패턴으로 바꿔보면

 

public abstract class CoffeTemplate(){

    final void makeCoffe(){
        boilWater();
        putEspresso();
        putIce();
        putEx();
    }

    abstract void boilWater(){
        System.out.println("boil water");
    }
    abstract void putEspresso(){
        System.out.println("put espresso");
    }
    abstract void putIce(){
        System.out.println("put ice");
    }

    abstract void putEx();
}

이렇게 공통되는 메소드와 서브클래스에서 확장해야 할 메소드를 정의 후

 

public class IceAmericano extends CoffeTemplate{


    @Override
    void putEx() {
        System.out.println("put syrup");
    }
}
public class IceLatte extends CoffeTemplate{

    @Override
    void putEx() {
        System.out.println("put milk");
    }
}
public class Main(){

    public static void main(String args[]) {

        IceAmericano iceAmericano = new IceAmericano();
        iceAmericano.makeCoffe();

        IceLatte iceLatte = new IceLatte();
        iceLatte.makeCoffe();
    }
}

템플릿 클래스를 상속받은 서브 클래스에서 각각 시럽, 우유를 넣도록 재정의 하고 템플릿의 makeCoffe() 메소드로 커피 생성

 

 

 

상태 패턴 State Pattern

특정 기능을 수행한 후 상태를 반환

 

동일한 메소드가 State 에 따라 다르게 동작할 때 사용할 수 있는 패턴

 

 

public interface State{
    State pushButton();
}

State 인터페이스

 

public class ButtonOn implements State{

    @Override
    public State pushButton() {
        return new ButtonOff();
    }
}
public class ButtonOff implements State{

    @Override
    public State pushButton() {
        return new ButtonOn();
    }
}

State 인터페이스를 상속받은 ButtonOn, Off 클래스

 

public class Television {

    private State state;

    public Television() {
        this.state = new ButtonOff();
    }

    public void pushButton() {
        state = state.pushButton();
    }
}
public class Main {

    public static void main(String[] args) throws Exception {
        
        Television tv = new Television();
        tv.pushButton();	// On
        tv.pushButton();	// Off
    }
}

같은 pushButton 메소드를 여러번 수행하더라도, 현재 State 에 따라 다른 동작을 하도록 구현이 가능 

 

 

 

반복자 패턴 Iterator Pattern

 

일련의 데이터 집합에 대해 순차적 접근 (순회) 를 지원하는 패턴

 

보통의 배열, 리스트같은 경우 반복문을 통해 순회 가능

But 트리같은 자료구조는 순서가 정해져 있지 않아서 각 요소를 어떤 기준으로 접근할 지 애매

 

이렇게 복잡한 구조의 자료에 대한 접근 방식이 공통화 되어 있다면 어떤 자료구조를 사용하더라도 Iterator 만 뽑아서 여러 전략으로 순회가 가능

 

 

!! 자바 컬렉션 프레임워크에서 각 컬렉션을 순회 가능한 것도 내부에 미리 Iterator 패턴이 적용되어 있기 때문 !!

 

Aggregate (인터페이스)

ConcreteIterator 객체를 반환하는 인터페이스 제공

 

ConcreteAggregate (클래스)

여러 요소들이 이루어져 있는 데이터 집합

 

Iterator (인터페이스)

집합 내의 요소들을 순서대로 검색하기 위한 인터페이스 제공

- hasNext() : 순회할 다음 요소가 있는지 확인

- next() : 요소를 반환하고 다음 요소를 반환 준비를 위한 커서 이동

 

ConcreteIterator (클래스)

ConcreteAggregate 가 구현한 메소드로부터 생성

ConcreteAggregate 의 컬렉션을 참조하여 순회

어떤 전략으로 순회할지에 대한 로직의 구체화

 

 

 

이를 코드로 구현해보면

 

 

public interface Aggregate {

    Iterator iterator();
}
public class ConcreteAggregate implements Aggregate{

    Object[] arr;   // 데이터의 집합
    int index = 0;


    public ConcreteAggregate(int size){
        this.arr= new Object[size];
    }

    public void add(Object o){
        if(index < arr.length){
            arr[index] = o;
            index++;
        }
    }

    @Override
    public Iterator iterator() {
        return new ConcreteIterator(arr);
    }
}

데이터 집합 객체 Aggregate 인터페이스와 그걸 상속받은 ConcreteAggregate 클래스

ConcreteAggregate 클래스는 생성자로 생성될 때 전달받은 size 만큼 크기를 가진 배열 생성 (데이터집합 생성)

add : 생성한 Object 배열에 각가의 요소들 삽입

iterator : 내부 컬렉션을 인자로 넣어 반복자 구현체를 반환

 

public interface Iterator {
    boolean hasNext();
    Object next();
}

반복체 객체 Iterator 인터페이스 

public class ConcreteIterator implements Iterator{

    Object[] arr;
    private int nextIndex = 0;

    public ConcreteIterator(Object[] arr){
        this.arr = arr;
    }

    @Override
    public boolean hasNext() {
        return nextIndex < arr.length;
    }

    @Override
    public Object next() {
        return arr[nextIndex++];
    }
}

 

nextIndex : 커서 (for문의 i변수)

concreteIterator : 전달받은 데이터 집합 컬렉션을 받아 필드에 참조

hasNext : 다음 요소가 있는지

next : 현재 요소를 반환하고 커서를 증가시켜 다음 요소를 바라보도록

 

public class Main {


    public static void main(String[] args) throws Exception {

        ConcreteAggregate concreteAggregate = new ConcreteAggregate(5);
        concreteAggregate.add(1);
        concreteAggregate.add(2);
        concreteAggregate.add(3);
        concreteAggregate.add(4);
        concreteAggregate.add(5);

        Iterator iterator = concreteAggregate.iterator();

        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

size 5 의 concreateAggregate 객체 생성 (데이터 집합)

값 삽입

데이터 집합의 반복자를 참조하는 iterator 변수 생성

while 문으로 iterator 의 다음 요소가 있는지, 있다면 현재 요소를 반환하고 다음 요소를 가리키는 동작을 반복하며 요소 출력

 

 

 

 

옵저버 패턴 Observer Pattern

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들이 이를 알아차리고, 자동으로 내용이 갱신되는 방식으로 1대 다 의존성을 정의

 

대부분 Subject 인터페이스와 Observer 인터페이스가 들어있는 클래스 디자인

 

 

interface Subject {
	registerObserver() // 옵저버 등록
	removeObserver() // 옵저버 삭제
	notifyObserver() // 옵저버에게 업데이트 알림
}

class SubjectImpl implements Subject {
	registerObserver() { ... }
	removeObserver() { ... }
	notifyObserver() { ... }

	getState() // 주제 객체는 상태를 설정하고 알기위한 겟터,셋터가 있을 수 있다.
	setState()
}

Subject 인터페이스와 구현체

 

 

interface Observer{ // 옵저버가 될 객체에서는 반드시 Observer 인터페이스를 구현해야함.
	update() // 주제의 상태가 바뀌었을때 호출됨
}

class ObserverImpl implements Observer {
	update() { 
		// 주제가 업데이트 될 때 해야하는 일
	}
}

옵저버 인터페이스와 구현체

 

 

현재날씨, 예보, 기상 통계를 불러오는 프로그램을 옵저버 패턴으로 구현해보면

 

public interface Subject {

    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    public void notifyObserver();
}
public interface Observer {

    public void update(float temperature, float humidity, float pressure);
}
public interface DisplayElement {

    public void display();
}

Subject, Observer, DisplayElement 인터페이스 정의

 

public class WeatherData implements Subject{

    private List<Observer> observers;
    private float humidity;
    private float temperature;
    private float pressure;


    public WeatherData() {
        observers = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    @Override
    public void notifyObserver() {
        for(Observer observer : observers){
            observer.update(temperature, humidity, pressure);
        }
    }

    public void measurementsChanged(){
        notifyObserver();
    }

    public void setMeasurements(float temperature, float humidity, float pressure){

        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature(){
        return temperature;
    }

    public float getHumidity(){
        return humidity;
    }

    public float getPressure(){
        return pressure;
    }
}

날씨 (온도, 습도, 기압) 정보를 담는 객체 클래스

observers :  옵저버를 담는 리스트

register, remove, notify 메소드 각각 재정의

 

measurementsChanged : 측정 정보가 변경될 경우 재정의된 notifyObservers 를 통해 데이터 업데이트

그 외 getter 메소드들

 

public class CurrentConditionsDisplay implements Observer, DisplayElement{


    private float temperature;
    private float humidity;
    private WeatherData weatherData;

    public CurrentConditionsDisplay(WeatherData weatherData){
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

    @Override
    public void display() {
        System.out.println("Current conditions : "+temperature + "F degrees and "+humidity+"% humidity");
    }
}

최근 상태 표시 클래스

WeaterData 를 바탕으로 최근 날씨 상태를 가지고 있는 클래스

역시 update, display 재정의를 통해 observer 로부터 데이터의 변동을 인지하고 변경된 데이터를 출력

 

 

public class ForecastDisplay implements Observer, DisplayElement{

    private float currentPressure = 29.92f;
    private float lastPressure;
    private WeatherData weatherData;


    public ForecastDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void display() {
        System.out.println("Forecast : ");
        if(currentPressure > lastPressure){
            System.out.println("Improving weather on the way !");
        }
        else if(currentPressure == lastPressure){
            System.out.println("More of the same");
        }
        else if(currentPressure < lastPressure){
            System.out.println("Watch out for cooler, rainy weather");
        }
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        lastPressure = currentPressure;
        currentPressure = pressure;
        display();
    }
}

날씨 예보를 출력하는 ForecastDisplay 클래스

display 에서 온도 차를 비교하여 적절한 문장 출력

update 에서 lastPressure 데이터를 최산화

 

public class StatisticsDisplay implements Observer, DisplayElement{

    private float maxTemp = 0.0f;
    private float minTemp = 200;
    private float tempSum = 0.0f;

    private int numReadings;
    private WeatherData weatherData;

    public StatisticsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        tempSum += temperature;
        numReadings++;

        if(temperature > maxTemp) {
            maxTemp = temperature;
        }
        if(temperature < minTemp){
            minTemp = temperature;
        }
        display();
    }


    @Override
    public void display() {
        System.out.println("Avg/Max/Min temperature = "+(tempSum/numReadings)+"/"+maxTemp+"/"+minTemp);
    }
}

날씨 통계 클래스

평균, 최대, 최소 온도를 display

최대, 최소 온도 갱신 update

 

public class Main {


    public static void main(String[] args) throws Exception {


        WeatherData weatherData = new WeatherData();

        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
        ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);

        weatherData.removeObserver(forecastDisplay);
        weatherData.setMeasurements(62, 90, 281f);

    }
}

메인함수

 

 

 

 

구조 패턴 Structural Pattern

 

데코레이터 패턴 Decorator Pattern

 

객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 해주는 패턴

추가 기능을 Decorator 클래스로 정의한 후 필요한 Decorator 객체를 조합함으로써 추가 기능의 조합을 설계하는 방식

 

말 그대로 어떤 오브젝트에 대해 데코레이션 하듯이 기능을 추가

 

ex)

도로 표시 방법 조합하기

 

네비게이션 프로그램에서 도로를 표시하는 기능을 구현한다고 할 때

네비게이션 프로그램에 따라 도로의 차선을 표시하는 기능을 추가적으로 구현하려면

 

public class RoadDisplay {

    public void draw(){
        System.out.println("basic road display");
    }
}
public class RoadDisplayWithLane extends RoadDisplay{

    public void draw(){
        super.draw();
        drawLane();
    }

    private void drawLane(){
        System.out.println("road with lane display");
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        
        RoadDisplay roadDisplay = new RoadDisplay();
        roadDisplay.draw();
        RoadDisplay roadDisplayWithLane = new RoadDisplayWithLane();
        roadDisplayWithLane.draw();
    }
}

 

 

기본 도로 표시 기능, 차선 표시 기능을 각각 구현한 모습

RoadDisplayWithLane 클래스에는 차선 표시 기능을 추가하기 위해 RaodDisplay 클래스를 상속하고 draw 메소드를 오버라이딩

 

 

여기서 또 다른 Display 를 추가로 구현하려고 하면?

RoadDisplayWithLane 과 같이 RoadDisplay 클래스를 상속받은 RoadDisplayWithXX 와 같은 클래스 구현

 

추가 기능이 많으면 많을 수록 그에 해당하는 클래스를 각각 구현해야 함

 

 

Lane, Traffic, Crossing 등..

위와 같이 원하는 기능을 조합한 기능을 구현하고자 하면 상당히 복잡할 수 있음

 

데코레이터 패턴을 사용하면 각 추가 기능별로 개별적인 클래스를 설계하고 기능을 조합할 때 각 클래스의 객체 조합을 이용하면 됨

 

 

각각의 기능을 말 그대로 데코 하듯이 조합하여 사용

 

 

public abstract class Display {

    public abstract void draw();
}
public class RoadDisplay extends Display{

    @Override
    public void draw(){
        System.out.println("basic road display");
    }
}
public class DisplayDecorator extends Display{

    private Display decoDisplay;

    public DisplayDecorator (Display decoDisplay){
        this.decoDisplay = decoDisplay;
    }

    @Override
    public void draw() {
        decoDisplay.draw();
    }
}
public class LaneDecorator extends DisplayDecorator{

    public LaneDecorator(Display decoDisplay) {
        super(decoDisplay);
    }

    @Override
    public void draw() {
        super.draw();
        drawLane();
    }

    private void drawLane(){
        System.out.println("lane display");
    }
}
public class TrafficDecorator extends DisplayDecorator{

    public TrafficDecorator(Display decoDisplay) {
        super(decoDisplay);
    }

    @Override
    public void draw() {
        super.draw();
        drawTraffic();
    }

    private void drawTraffic(){
        System.out.println("traffic display");
    }
}
public class Main {
    public static void main(String[] args) throws Exception {


        Display display  = new RoadDisplay();
        display.draw();

        Display laneDisplay = new LaneDecorator(new RoadDisplay());
        laneDisplay.draw();

        Display trafficDisplay = new TrafficDecorator(new RoadDisplay());
        trafficDisplay.draw();
    }
}

 

DisplayDecorator 를 상속받은 하위 기능 클래스들을 구현

 

 

이와 같은 구조로 구현 가능

 

Lane 과 Traffic 을 함께 표현하고 싶다면?

 

        Display laneTrafficDisplay = new TrafficDecorator(new LaneDecorator(new RoadDisplay()));
        laneTrafficDisplay.draw();

 

 

이와 같이 사용 가능

 

1. 가장 먼저 생성된 RoadDisplay 객체의 draw 메소드 실행

2. 첫 번째 추가 기능인 LaneDecorator 클래스의 drawLane 메소드 실행

3. 두 번째 추가 기능인 TrafficDecorator 클래스의 drawTraffic 메소드 실행

 

 

 

 

 

프록시 패턴 Proxy Pattern

 

구체적인 인터페이스를 사용하고 실행시킬 클래스에 대한 객체가 들어갈 자리에 Proxy 객체를 대신 투입

클라이언트가 어떤 일에 대한 요청을 하면, Proxy 가 대신 Subject 의 request 메소드를 호출

그 반환값을 클라이언트에게 전달

 

public interface Service {
    String run();
}
public class ServiceImpl implements Service{

    @Override
    public String run() {
        return "run something";
    }
}
public class Proxy implements Service{

    Service service;

    @Override
    public String run() {

        service = new ServiceImpl();
        return service.run();
    }
}
public class Main {
    public static void main(String[] args) throws Exception {


        Service proxyService = new Proxy();
        System.out.println(proxyService.run());
    }
}

 

기본적인 Proxy 패턴의 구조

ServiceImpl 클래스에서 바로 run 메소드를 호출하지 않고 Proxy 를 거쳐서 실행

기능에는 변함이 없지만, 제어의 흐름을 관리하기 위해서 사용

 

 

ex)

용량이 큰 이미지와 글이 함께 있는 문서를 화면에 띄우기 위한 프로그램

텍스트는 용량이 작아서 빠르게 출력이 가능하지만, 이미지는 큰 용량으로 느리게 출력되는 상황에서

텍스트, 이미지의 출력을 동시에 구현하고 싶은 상황

 

public interface Image {

    public void displayImage();
}
public class OriginImage implements Image{

    private String fileName;

    public OriginImage(String fileName) {
        this.fileName = fileName;
    }

    private void loadFromDisk(String fileName){
        System.out.println("loading file .. "+fileName);
    }

    @Override
    public void displayImage() {
        System.out.println("display "+fileName);
    }
}
public class ProxyImage implements Image{

    private String fileName;
    private OriginImage originImage;

    public ProxyImage(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void displayImage() {
        if(originImage == null){
            originImage = new OriginImage(fileName);
        }
        originImage.displayImage();
    }
}
public class Main {
    public static void main(String[] args) throws Exception {


        Image firstImage = new ProxyImage("test1");
        Image secondImage = new ProxyImage("test2");

        firstImage.displayImage();
        secondImage.displayImage();
    }
}

 

OriginImage 클래스의 displayImage 메소드를 바로 호출하지 않고, ProxyImage 클래스를 거쳐 호출함으로써

displayImage 를 호출하기 전, 후에 대한 동작 제어가 가능

 

 

 

 

컴포지트 패턴 Composite Pattern

합성한 객체의 집합

트리 구조를 작성하고 싶을 때 사용

전체-부분 관계를 표현

폴더 시스템에 비유 가능 (계층적 구조)

 

 

상위 클래스를 두고 하위에 Leaf 클래스

Leaf 클래스들은 Composite 형태로 Leaf 를 담아두는 복합체나, 단순한 하나의 Leaf 일 수도 있음

하위 Leaaf 들은 동작하는 메소드를 상속, 재정의 하여 사용

 

Depth 를 가진 구조

 

 

ex)

 

위와 같은 구조의 파일 시스템 구현

폴더가 2개 있고, 한 폴더는 사위 폴더 하에 있으며, 상위 폴더는 하나의 파일도 보유

하위 폴더에는 2개의 파일 존재

 

 

public interface FileSystem {

    public int getSize();
    public void remove();
}
public class File implements FileSystem{

    private String name;
    private int size;

    public File(String name, int size) {

    }

    @Override
    public int getSize() {
        System.out.println("file size : "+size);
        return size;
    }

    @Override
    public void remove() {
        System.out.println("delete file");
    }
}
public class Folder implements FileSystem{

    private String name;
    private List<FileSystem> included = new ArrayList<>();

    public Folder(String name) {
        this.name = name;
    }

    public void add(FileSystem fileSystem){
        included.add(fileSystem);
    }

    @Override
    public int getSize() {

        int total = 0;
        for(FileSystem include : included){
            total += include.getSize();
        }
        System.out.println(name+" size : "+total);
        System.out.println("------------------");
        return total;
    }

    @Override
    public void remove() {
        for(FileSystem include : included){
            include.remove();
        }

        System.out.println("delete "+name);
        System.out.println("------------------");
    }
public class Main {
    public static void main(String[] args) throws Exception {


        Folder schoolFolder = new Folder("school");
        Folder firstGradeFolder = new Folder("first grade folder");
        Folder secondGradeFolder = new Folder("second grade folder");

        schoolFolder.add(firstGradeFolder);
        schoolFolder.add(secondGradeFolder);

        File enterPhoto = new File("graduated photo", 256);
        firstGradeFolder.add(enterPhoto);

        Folder firstSemesterFolder = new Folder("first semester");
        Folder secondSemesterFolder = new Folder("second semester");

        secondGradeFolder.add(firstSemesterFolder);
        secondGradeFolder.add(secondSemesterFolder);

        File syllabus = new File("syllabus", 120);
        secondSemesterFolder.add(syllabus);

        Folder project = new Folder("project");
        secondSemesterFolder.add(project);

        File finalResult = new File("final result", 560);

        project.add(finalResult);

        schoolFolder.getSize();
        schoolFolder.remove();
    }
}

 

 

위와 같은 트리 구조의 FileSystem 구현

 

 

 

 

어댑터 패턴 Adapter Pattern

 

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환

인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 사용 가능

 

나중에 인터페이스가 바뀌더라도 그 변경 내역은 어댑터에 캡슐화 되기 때문에 클라이언트의 변경 X

 

- 외부 라이브러리 클래스를 사용하고 싶은데, 클래스 인터페이스가 다른 코드와 호환되지 않을 때 이를 해결하기 위해 어댑터 사용 가능

- 여러 자식 클래스가 있는데, 부모 클래스를 수정하기에는 호환성이 문제가 될 때 해결 가능

 

 

어댑터 패턴은 크게 두 가지로 분류 가능

 

1. 객체 어댑터

합성된 멤버에게 위임을 이용한 어댑터 패턴

자기가 해야할 일을 클래스 멤버 객체의 메소드에 다시 시킴으로써 목적을 달성하는것을 위임이라고 함

합성을 활용했기 때문에 런타임중에 Adaptee(Service) 가 결정되어 유연한 구조

Adaptee(Service) 객체를 필드변수로 저장해야 하기 때문에 메모리 차지

 

 

2. 클래스 어댑터

클래스 상속을 이용한 어댑터 패턴

Adaptee(Service) 를 상속했기 때문에 객체 구현 없이 바로 코드 재사용 가능

상속은 대표적으로 기존 구현 코드를 재사용하는 방법이지만, 자바에서는 다중상속 불가 문제때문에 전반적으로 권장하지 않음

 

 

ex)

프로그램 엔진을 교체하고 호환시키기

A 회사에서 개발한 Sort 엔진 솔루션 구매해서 우리 회사의 Sort 머신에 탑재하여 사용하고 있는 상태

 

 

 

public interface SortEngine {

    public void setList();
    public void sort();
    public void reverseSort();
    public void printSortListPretty();
}
public class SortEngineAImpl implements SortEngine{

    @Override
    public void setList() {

    }

    @Override
    public void sort() {

    }

    @Override
    public void reverseSort() {

    }

    @Override
    public void printSortListPretty() {

    }
}
public class SortingMachine {

    SortEngine engine;

    public void setEngine(SortEngine engine){
        this.engine = engine;
    }

    public void sort(){
        engine.setList();
        engine.sort();
        engine.printSortListPretty();
        engine.reverseSort();
        engine.printSortListPretty();
    }


    public static void main(String[] args){
        SortingMachine machine = new SortingMachine();
        machine.setEngine(new SortEngineAImpl());
        machine.sort();
    }
}

 

 

여기서 A 회사의 Sort 엔진 성능이 맘에 들지 않아, B 회사로 갈아타려고 한다

B 회사의 엔진 명세를 살펴보니, 기존 Sort 엔진과는 동작 메서드 시그니처가 다르고 지원하지 않는 메서드도 존재

 

public class SortEngineB {

    public void setList(){}

    public void sort(boolean isReverse){}
}

Sort 인터페이스를 상속받아 사용하고 있었기 때문에 B 사의 엔진에 인터페이스를 implements 불가능 

>> 이는 큰 수정이 필요

또한 printSortListPretty 메소드의 부재

 

 

가장 직관적인 방법은 SortingMachine 클래스를 수정하는 방법

 

 

기존 SortEngine 인터페이스의 메소드를 비우고

public class SortingMachine {

    SortEngine engine;

    public void setEngine(SortEngine engine){
        this.engine = engine;
    }

    public void sort(){

        SortEngineAImpl engineA = (SortEngineAImpl) this.engine;
        SortEngineBImpl engineB = (SortEngineBImpl) this.engine;

        engineA.setList();
        engineA.printSortListPretty();

        engineB.setList();
        engineB.sort(false);
    }


    public static void main(String[] args){

        SortingMachine machine = new SortingMachine();
        machine.sort();
    }
}

처럼 각각 다운캐스팅하여 호환

문제는 나중에 엔진을 또 교체해야 하는 상황이 발생한다면, 코드를 또 전면적으로 수정해야 함

 

 

1. 객체 어댑터 패턴 적용

우리 회사의 Sort 엔진에서 이요하던 인터페이스를 손대지 않고 별도의 어댑터 SortEngineAdapter 클래스를 만들어 호환작업

 

public interface SortEngine {

    public void setList();
    public void sort();
    public void reverseSort();
    public void printSortListPretty();
}

인터페이스는 그대로 놔두고

 

public class SortEngineAdapter implements SortEngine{

    SortEngineAImpl engineA;
    SortEngineBImpl engineB;

    public SortEngineAdapter(SortEngineAImpl engineA, SortEngineBImpl engineB) {
        this.engineA = engineA;
        this.engineB = engineB;
    }

    @Override
    public void setList() {
        engineB.setList();
    }

    @Override
    public void sort() {
        engineB.sort(false);
    }

    @Override
    public void reverseSort() {
        engineB.sort(true);
    }

    @Override
    public void printSortListPretty() {
        engineA.printSortListPretty();
    }
}

SortEngine 인터페이스를 상속받은 어댑터를 구현

printSortListPretty 와 같은 메소드는 A 엔진의 메소드를 사용하고 나머지는 속도가 빠른 B 엔진 사용

 

public class SortingMachine {

    SortEngine engine;

    public void setEngine(SortEngine engine){
        this.engine = engine;
    }

    public void sort(){

        engine.setList();
        engine.sort();
        engine.printSortListPretty();
        engine.reverseSort();
        engine.printSortListPretty();
    }


    public static void main(String[] args){

        SortEngine adapter = new SortEngineAdapter(new SortEngineAImpl(), new SortEngineBImpl());

        SortingMachine machine = new SortingMachine();
        machine.setEngine(adapter);
        machine.sort();
    }
}

위와 같이 사용 가능

 

 

이렇게 구현하면, 나중에 엔진을 또 교체하거나 추가한다고 하더라도 Adpter 클래스만 적절하게 수정하면 되기 때문에 유지보수 용이

기존 클라이언트 (SortingMachine) 클래스의 코드 수정 필요 X

 

 

 

2. 클래스 어댑터 패턴 적용

 

public class SortEngineAdapter extends SortEngineBImpl implements SortEngine{

    @Override
    public void setList() {
        super.setList();
    }

    @Override
    public void sort() {
        sort(false);
    }

    @Override
    public void reverseSort() {
        sort(true);
    }

    @Override
    public void printSortListPretty() {
        // A 클래스의 원본 printSortListPretty 메소드 알고리즘 로직을 똑같이 구현
    }
}

B 엔진 클래스와 SortEngine 인터페이스를 각각 상속받은 어댑터 (SortEngineAdapter) 구현

 

public class SortingMachine {

    SortEngine engine;

    public void setEngine(SortEngine engine){
        this.engine = engine;
    }

    public void sort(){

        engine.setList();
        engine.sort();
        engine.printSortListPretty();
        engine.reverseSort();
        engine.printSortListPretty();
    }


    public static void main(String[] args){

        SortEngine adapter = new SortEngineAImpl();
        SortingMachine machine = new SortingMachine();

        machine.setEngine(adapter);
        machine.sort();
    }
}

SortingMachine 객체에 원본 엔진 대신 어댑터를 할당하고 사용

 

>> 클래스 다중상속 문제때문에 권장하지는 않는 방법

 

 

 

퍼사드 패턴 Facade Pattern

서브 시스템을 보다 쉽게 쓸 수 있도록 높은 수준의 인터페이스를 정의하는 작업

강력한 결합 구조를 해결하기 위해 코드 의존성 줄이고 느슨한 결합으로 구조 변경

메인 시스템 < 퍼사드 패턴 > 서브시스템  이렇게 중간에 새로운 인터페이스 계층을 추가하여 시스템 사이의 의존성 해결

인터페이스 계층은 메인 시스템과 서브 시스템의 견결 관계를 대신 처리

객체 내부 구조 상세히 알 필요 X

퍼사드 패턴은 시스템 연결성, 종속성 최소화를 목적으로 함

최소 지식 원칙 : 최소 지식만 적용해 객체 상호작용을 구현하면 유지보수 용이

 

 

 

ex)

자동차의 구성품을 퍼사드 패턴으로 구현

 

 

public interface Car {

    public void open(String key);
    public void drive(String key);
    public void stop();
    String getName();
}

Car 인터페이스

public class CarKey {

    private String key;

    public CarKey(String key){
        this.key = key;
    }

    public boolean turns(String key){
        return this.key.equals(key);
    }
}

Door 인터페이스에서 사용하는 CarKey 클래스

 

public enum EngineStatus {

    DRIVE("drive", "운행"),
    STOP("stop", "정지");

    private final String code;
    private final String displayName;

    EngineStatus(String code, String displayName) {
        this.code = code;
        this.displayName = displayName;
    }

    public String getCode(){
        return code;
    }
    public String getDisplayName(){
        return displayName;
    }
}

Engine 인터페이스에서 사용하는 EngineStatus 열거형

 

public interface Door {
    public void lock(String key);
    public void unlock(String key);

    CarKey getKey();
}
public interface Engine {

    public void start();
    public void stop();
    EngineStatus status();
}

Door, Engine 인터페이스

 

public class DoorImpl implements Door{

    private boolean lock;
    private CarKey key;

    public DoorImpl(CarKey key) {
        this.key = key;
        this.lock = true;
    }

    @Override
    public void lock(String key) {
        if(!this.key.turns(key)){
            throw new CarKeyNotMatchException();
        }
        System.out.println("door is close");
    }

    @Override
    public void unlock(String key) {
        if(!this.key.turns(key)){
            throw new CarKeyNotMatchException();
        }
        System.out.println("door is open");
    }

    @Override
    public CarKey getKey() {
        return key;
    }
}
public class EngineImpl implements Engine{

    EngineStatus engineStatus;


    public EngineImpl() {
        this.engineStatus = EngineStatus.STOP;
    }

    @Override
    public void start() {
        this.engineStatus = EngineStatus.DRIVE;
    }

    @Override
    public void stop() {
        this.engineStatus = EngineStatus.STOP;
    }

    @Override
    public EngineStatus status() {
        return this.engineStatus;
    }
}

Door, Engine 인터페이스의 구현체

 

 

 

public class CarKeyNotMatchException extends IllegalArgumentException{

    public CarKeyNotMatchException() {
        super("key does not match");
        System.out.println("CarKeyNotMatchException");
    }
}

 

Key 일치하지 않을 때 발생하는 Exception 클래스

 

public class CarImpl implements Car{

    private Engine engine;
    private String name;
    private Door door;

    public CarImpl(Engine engine, String key) {
        this.engine = engine;
        this.name = "Hyundai";
        this.door = new DoorImpl(new CarKey(key));
    }

    @Override
    public void open(String key) {
        door.unlock(key);
    }

    @Override
    public void drive(String key) {

        boolean authorized = this.door.getKey().turns(key);

        if(authorized){
            engine.start();
            this.updateDashboardDisplay();
            door.lock(key);
        }
        else{
            throw new CarKeyNotMatchException();
        }
    }

    @Override
    public void stop() {
        engine.stop();
        this.updateDashboardDisplay();
    }

    @Override
    public String getName() {
        return this.name;
    }

    private void updateDashboardDisplay(){
        System.out.println(getName()+" "+engine.status().getDisplayName());
    }
}

Car 구현체

 

 

public class CarTest {

    public void driveTest(){
        String key = "CAR_SECRET_KEY";
        Car car = new CarImpl(new EngineImpl(), key);

        car.open(key);
        car.drive(key);
        car.stop();
    }

    public void driveInvalidTest(){
        String key = "CAR_SECRET_KEY";
        Car car = new CarImpl(new EngineImpl(), key);
        assertThrows(CarKeyNotMatchException.class, ()->{car.open("INVALID KEY");});
    }
}

테스트 코드

 

이와 같은 퍼사드 패턴 구조로 표현 가능

'끄적 > ?' 카테고리의 다른 글

HashMap Key에 Object  (0) 2023.09.17
업캐스팅 다운캐스팅  (0) 2023.08.27
Generic  (0) 2023.08.27
Java ArrayList와 List 차이  (0) 2023.08.27
String, StringBuffer, StringBuilder  (0) 2023.08.27

HashMap 자료구조에서 Key 로 객체를 활용하면??

 

 

public class SampleClass {

    private int code;

    public SampleClass(int code){
        this.code = code;
    }

    public int getCode() {
        return code;
    }
}

위와 같은 샘플 클래스를 만들고

 

        SampleClass sample1 = new SampleClass(1);
        SampleClass sample2 = new SampleClass(2);
        SampleClass sample3 = new SampleClass(1);

        HashMap<SampleClass, String> map = new HashMap<>();


        map.put(sample1, "first");
        map.put(sample2, "second");
        map.put(sample3, "third");

        map.forEach(
                (key, value) -> {System.out.println(key+", "+value+", "+key.getCode());}
        );

 

나는 sample1 과 sample3 가 같음을 의도했지만

 

 

SampleClass@41629346, first, 1
SampleClass@6d311334, third, 1
SampleClass@404b9385, second, 2

 

결과는 이렇게 출력

 

왜냐? sample1 과 sample3 내부의 값이 같다고 같은 객체가 아니기 때문

객체가 참조되는 메모리의 주소가 서로 다르기때문에 다른 Key 값으로 인식

 

그럼 의도대로 동작하게 하려면?

!! equals, hashcode 메소드를 재정의 !!

 

 

equals 메소드

동일성 비교

객체 인스턴스의 주소 값을 비교

 

hashcode 메소드

동등성 비교

equals 메소드를 이용해 객체 내부의 값을 비교

 

 

 

public class SampleClass {

    private int code;

    public SampleClass(int code){
        this.code = code;
    }

    public int getCode() {
        return code;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        SampleClass that = (SampleClass) o;
        return code == that.code;
    }

    @Override
    public int hashCode() {
        return Objects.hash(code);
    }
}

 

 

 

equlas 메소드는

내부 값(code) 을 비교하여 두 객체가 동등한지, 아닌지 판단하도록 구현

 

hashcode 메소드는

내부 값(code) 이 같다면 동일한 hash 값을 반환하도록 구현

 

이로써 내가 원하는

내부 값이 같으면 같은 객체다 

라는 동작이 구현 가능해진다

 

SampleClass@20, third, 1
SampleClass@21, second, 2

 

이렇게 sample1 과 sample3 가 동일한 객체, Key 로 판단되어 값이 변화하게 된다

'끄적 > ?' 카테고리의 다른 글

Design Patten  (0) 2023.09.19
업캐스팅 다운캐스팅  (0) 2023.08.27
Generic  (0) 2023.08.27
Java ArrayList와 List 차이  (0) 2023.08.27
String, StringBuffer, StringBuilder  (0) 2023.08.27

 

캐스팅 (Casting)

타입을 변환하는 것

쉽게 말해 형변환

 

한 데이터 유형을 다른 데이터 유형으로 변환하는 프로세스를 Typecasting 혹은 Upcasting 이라고 하며

Downcasting 은 객체 유형 캐스팅

 

Java 에서는 객체도 데이터타입처럼 형 변환이 가능

 

Typecasting 은 변수가 함수에 의해 올바르게 처리되는지 여부를 확인하는데 사용

Upcasting 은 암시적, 명시적으로 수행 가능하지만, Downcasting 은 암시적으로 사용할 수 없다

 

 

 

 

 

업캐스팅 (Upcasting)

하위 객체가 상위 클래스로 Typecast 되는 유형

업캐스팅을 하면 슈퍼클래스 변수, 메서드를 자식 클래스에서 쉽게 접근 가능

 

쉽게 말해

서브클래스의 객체가 슈퍼 클래스로 형 변환 되는 것

 

class Parent{

    void printData(){
        System.out.println("method of parent class");
    }
}

class Child extends Parent{

    void printData(){
        System.out.println("method of child class");
    }

    void printChildData(){
        System.out.println("method of only child class");
    }
}
        Parent parent;
        Child child = new Child();

 
        // parent = (Parent) child;	괄호 생략 가능
        parent = child;
        
        // parent.printChildData();	에러 발생

<업캐스팅은 형변환 괄호 생략 가능>

 

위 코드에서 슈퍼클래스 타입의 parent 객체가 서브클래스 객체인 child 를 가리키는 것이 업캐스팅

업캐스팅을 통해 Parent 타입의 parent 는 Child 객체를 가리킴

parent 는 Parent 타입이기 때문에 슈퍼클래스 객체의 클래스 멤버에만 접근이 가능하다.

 

 

 

업캐스팅을 하게 되면, 슈퍼클래스 멤버로 멤버 개수가 한정되기 때문에 서브클래스의 모든 멤버에 접근 X

쉽게 말해 서브클래스, 슈퍼클래스에서 공통된 것만 사용 가능, 서브클래스에만 있는건 사용 X

 

 

왜?

공통적으로 할 수 있는 부분을 만들어 간단하게 다루기 위함

상속 관계에서 상속 받은 서브클래스의 개수와 상관없이 하나의 인스턴스로 묶어서 관리가 가능하기 때문

 

 

Rectangle[] r = new Rectangle[];
r[0] = new Rectangle();
r[1] = new Rectangle();

Triangle[] t = new Triangle[];
t[0] = new Triangle();
t[1] = new Triangle();

Circle[] c = new Circle[];
c[0] = new Circle();
c[1] = new Circle();

위와 같은 코드에서는, 사각형, 삼각형, 원 모두 각자의 객체를 생성하여 사용

 

Shape[] s = new Shape[];
s[0] = new Rectangle();
s[1] = new Rectangle();
s[2] = new Triangle();
s[3] = new Triangle();
s[4] = new Circle();
s[5] = new Circle();

업캐스팅을 활용하면 위와 같이 하나의 인스턴스로 여러 타입을 묶어서 활용 가능

유지보수에 매우 좋음

 

 

 

다운캐스팅 (Downcasting)

업캐스팅 객체에서 서브클래스에만 있는 고유한 메서드를 실행하고 싶으면?

>> 그래서 다운캐스팅이 필요

 

자신의 특성을 잃은 서브클래스 객체를 다시 복구시켜 주는 것

업캐스팅 된 것을 다시 원상태로 돌리는 것을 의미

 

** 단순하게 업캐스팅의 반대 개념이 아님

슈퍼클래스로 업캐스팅 된 서브클래스를 복구하여 본인의 필드와 기능을 회복하기 위한 것

 

 

다운캐스팅에서는 괄호 생략이 불가능하다

 

 

Parent parent = new Child();
Child child = (Child) parent;

child.printChildData();

parent 객체는 Child 로 업캐스팅

이후 child 객체를 생성하며 parent 객체를 다시 다운캐스팅

Child 클래스로 다운캐스팅 된 child 객체는 다시 Child 클래스만의 메서드인 printChildData() 사용 가능

 

 

 

주의

 

업캐스팅 되지 않은 객체일 경우 다운캐스팅을 시도하면 ClassCastException 에러 발생

Parent parent = new Parent("name of parent");

Child child = (Child) parent;

위와 같은 코드에서는 

Exception in thread "main" java.lang.ClassCastException: class Parent cannot be cast to class Child (Parent and Child are in unnamed module of loader 'app')

위와 같은 에러가 발생

 

 

IDE 상에서 빨간줄로 에러가 표시되지 않으므로 코드 작성시에 매우매우 주의해야 함

 

'끄적 > ?' 카테고리의 다른 글

Design Patten  (0) 2023.09.19
HashMap Key에 Object  (0) 2023.09.17
Generic  (0) 2023.08.27
Java ArrayList와 List 차이  (0) 2023.08.27
String, StringBuffer, StringBuilder  (0) 2023.08.27

 

제너릭 (Generic) ?

일반적인 이란 뜻

 

 

List<Integer> list = new ArrayList<Integer>();


List<String> list = new ArrayList<String>();

위와 같이 ArrayList 클래스 인스턴스를 생성하는데, Integer, String 등 자료형을 유연하게 선택 가능

 

 

 

 

 

 

제너릭을 사용하지 않을 경우

public class ExampleClass {
    private int exampleData;

    public void setExampleMethod(int data){
        this.exampleData = data;
    }

    public int get(){
        return exampleData;
    }
}

위의 ExampleClass 는 int 형의 데이터만 사용할 수 있다

 

내가 만약 String 형 데이터를 활용하는 ExampleClass 를 만들고 싶으면 새로운 클래스를 만들거나

String 타입 데이터를 받는 필드를 새로 추가하여 활용해야 하는데, 이는 유지/보수에 매우 좋지 않다

 

 

 

제너릭을 사용하지 않고 여러 타입을 받는 클래스

public class ExampleClass {
    private Object exampleData;

    public void setExampleMethod(Object data){
        this.exampleData = data;
    }

    public Object get(){
        return exampleData;
    }
}

위와 같은 클래스는 여러 자료형을 활용 가능하지만, 활용 할 때마다 해당 타입에 맞는 타입 캐스팅이 필요

 

ExampleClass e = new ExampleClass();
e.setExampleMethod(10);
int m = (int)e.get();

위와 같이 int 형 데이터를 활용하려면, (int) 로 타입 캐스팅이 필요

 

 

 

제너릭 클래스 사용할 경우

public class ExampleClass<T>{

    private T exampleData;

    public void setExampleMethod(T data){
        this.exampleData = data;
    }

    public T get(){
        return exampleData;
    }
}

 

 

위와 같이 T 같은 키워드를 통해 제너릭 타입 클래스로 선언

 

ExampleClass<Integer> e = new ExampleClass<Integer>();
e.setExampleMethod(10);
int m = e.get();

위와 같이 <>안에 타입을 지정하여 해당 타입만을 활용하는 인스턴스를 생성 가능

타입캐스팅 불필요

 

 

 

** 키워드 (규칙은 없지만, 일반적으로 사용되는 대문자 알파벳)

타입인자 설명
<T> Type
<E> Element
<K> Key
<N> Number
<V> Value
<R> Result

 

'끄적 > ?' 카테고리의 다른 글

HashMap Key에 Object  (0) 2023.09.17
업캐스팅 다운캐스팅  (0) 2023.08.27
Java ArrayList와 List 차이  (0) 2023.08.27
String, StringBuffer, StringBuilder  (0) 2023.08.27
Java 메모리 구조, JVM 동작 방식  (0) 2023.08.27

 

 

List 는 인터페이스

ArrayList 는 클래스 (List 인터페이스를 상속받아 만들어진 >> List 의 기능 모두 사용 가능)

 

 

List 는 Collection 의 하위 인터페이스

중복된 값을 저장할 수 있는 순서가 지정된 개체 모음

 

List 인터페이스는 ArrayList, LinkedList, Vector, Stack 클래스로 구현 가능

다양한 클래스를 구현하여 List 의 인스턴스 생성, 활용 가능

 

 

ArrayList 는 java.util 패키지에 포함된 Collection 프레임워크의 일부

동적인 배열 제공하는 클래스

int, char 등의 기본 데이터타입을 활용하기 위해선 wrapper 클래스 사용 (Integer, Char)

 

 

 

List ArrayList
인터페이스 클래스
인스턴스화 불가능 (인터페이스이기 때문에) 인스턴스화 가능
Collection framework 의 확장 AbstractList 클래스 확장, List 인터페이스 구현
List 인터페이스는 Index 와 연결된 요소 List 를 생성 객체를 포함하는 동적 배열을 만드는데 사용
시퀀스로 저장되는 요소 컬렉션 생성, Index 사용하여 식별 및 접근 배역이 동적으로 커질 수 있는 객체배열 

 

ArrayList list = new ArrayList<>();

위와 같이 ArrayList 인스턴스를 생성하면, 나중에 데이터의 용도가 바뀌어 삽입/삭제가 유리한 LinkedList 구조로 변경해야 할 때, ArrayList 로 선언된 모든 부분을 LinkedList 로 수정해야 함

 

또한 ArrayList 에서는 지원하지만, LinkedList 에선 지원하지 않는 메서드는 더이상 사용하지 못함

이는 코드 유지/보수에 있어 유연하지 못한 구조

 

 

List list = new ArrayList<>();

위와 같이 사용한다면, 같은 상황에서 new ArrayList 인스턴스 생성 부분만 수정하고, 다른 부분에 대한 변경 필요 X

이게 바로 업캐스팅 하여 사용하는 이유

 

추가적으로

대부분의 경우 ArrayList 만 제공하는 메서드는 사용을 지양한다

>> 다른 List 로 바꿔야 할 경우 수정이 힘들기 때문에

List 로 선언해야 List 에서 제공하는 메서드까지 사용이 가능하기 때문에

 

 

'끄적 > ?' 카테고리의 다른 글

업캐스팅 다운캐스팅  (0) 2023.08.27
Generic  (0) 2023.08.27
String, StringBuffer, StringBuilder  (0) 2023.08.27
Java 메모리 구조, JVM 동작 방식  (0) 2023.08.27
추상클래스 인터페이스  (0) 2023.03.12

 

String

불변성 (immutable) 을 지님

한 번 생성된 문자열을 읽기만 가능

 

String 의 + 연산은 값을 변경하는 것이 아닌 새로운 객체를 생성하는 것

 

1. 기존 String 의 값을 가진 새로운 String 객체를 StringBuilder 를 통해 기존 값에 + 연산 수행한 문자열을 append 한 객체 생성

2. 해당 객체의 값을 기존 String 변수가 참조 (메모리 주소가 변함)

 

 

String s = "ab";
s+="cd";

 

위 코드의 연산의 경우

String newString = new StringBuilder("ab").append("cd").toString();

 

 

실제로는 이 코드처럼 동작

 

 

String number = "0"
for(int i=0; i<10000; i++){
	number += Integer.toString(i);
}

위 처럼 반복문에서 사용할 경우

 

String number = "0";

for(int i=0; i< 10000; i++){
	number = new StringBuilder(number).append(Integer.toString(i)).toString();
}

실제론 위와 같이 동작

많은 수의 String 객체가 생성되고 활용되지 않아 GC 의 회수 대상이 되므로

프로그램 성능 저하를 일으킬 수 있음

 

 

 

StringBuilder

가변성 (Mutable)

값이 할당되있더라도 한 번 더 값을 할당하면 할당 공간이 바뀜 >> 값 변경 가능

 

 

AbstractStringBuilder 추상 클래스를 상속받아 구현

 

value : 문자열의 값을 저장하는 변수 (Byte 배열)

count : 현재 문자열의 크기의 값을 가지는 int 형 변수

 

append : 현재 문자열에 다른 문자열 추가

public AbstractStringBuilder append(String str) {
        if (str == null) {
            return appendNull();
        }
        int len = str.length();
        ensureCapacityInternal(count + len);
        putStringAt(count, str);
        count += len;
        return this;
    }

위와 같이 구성

 

추가할 문자열의 길이만큼 현재 문자열을 저장하는 Byte 배열의 공간을 확장하고, 그 공간에 추가할 문자열 삽입

값이 변경되어도 같은 주소 공간을 참조

 

 

 

StringBuffer

StringBuilder 와 동일한 동작, 동일한 키워드

StringBuilder 와의 차이점은 동기화 (Synchronization) 의 유무

 

StringBuffer 는 Synchronized 키워드를 통해 멀티스레드 환경에서의 안정성 보유

 

StringBuilder 는 동기화 X

단일스레드에서는 성능 우수 (StringBuffer 대비)

 

 

 

요약

 

 

 

'끄적 > ?' 카테고리의 다른 글

업캐스팅 다운캐스팅  (0) 2023.08.27
Generic  (0) 2023.08.27
Java ArrayList와 List 차이  (0) 2023.08.27
Java 메모리 구조, JVM 동작 방식  (0) 2023.08.27
추상클래스 인터페이스  (0) 2023.03.12

 

 

코드 영역

실행할 프로그램의 코드가 저장되는 영역

텍스트 영역이라고도 함

CPU는 코드 영역에 저장된 명령어를 하나씩 처리

 

 

데이터 영역

전역 변수, 정적 변수 (static) 저장 영역

프로그램 시작과 동시에 할당

프로그램 종료 시 소멸

 

스택 영역

함수의 호출과 관련된 지역변수, 매개변수가 저장되는 영역

함수의 호출과 함께 할당

함추 호출이 완료되면 반환

메모리의 높은 주소에서 낮은 주소로 할당

 

힙 영역

사용자가 관리해야 하는 영역

사용자에 의해 메모리 공간이 동적으로 할당되는 공간

 

배열을 예로 들면, 

 

int a = 10;
int array[10];

위 코드는 동작하지 않는다

a 라는 변수의 값은 런타임에 초기화되는데, array 배열 변수는 컴파일 과정에서 선언되고 크기를 할당해야 하기 때문

 

int a = 10;
int array = new int[a];

 

 

위와 같이 런타임 단계에서 힙 영역에 array 객체를 동적으로 할당하여 사용

 

 

 

 

 

JVM 동작방식

 

 

1. Java 프로그램 실행 시, JVM 은 OS 로부터 메모리 할당

2. Java 컴파일러가 소스코드를 자바 바이트코드 (.class) 로 컴파일

3. Class Loader 를 통해 JVM Runtime Data Area 로 로딩

4. Runtime Data Area 에 로딩 된 .class 들은 Execution Engine 을 통해 해석

5. 해석된 바이트 코드는 Runtime Data Area 각 영역에 배치되어 수행

이 과정에서 Excution Engine 에 의해 GC 작동, 스레드 동기화 이루어짐

 

 

 

JVM 구조

클래스로더 (Class Loader)

 

자바는 동적으로 클래스 로딩

런타임에서 모든 코드가 JVM과 연결

동적으로 클래스를 로딩해 주는 역할이 바로 Class Loader 

.java 파일을 컴파일한 결과인 .class 파일을 묶어서 OS 로부터 할당된 메모리영역인 Runtime Data Are 로 적재

 

실행 엔진 (Execution Engine)

Method Area 의 바이트코드를 실행 엔진에 제공

정의된 내용되로 바이트코드 실행

이 때, 로드된 바이트코드를 실행하는 런타임 모듈이 실행엔진

 

실행엔진은 바이트코드를 명령어 단위로 읽어서 실행

 

 

가비지 콜렉터 (Garbage Collector)

 

사용되지 않는 메모리 자동 회수

힙 영역에 적재된 객체들 중 더이상 참조도지 않는 객체를 탐색하고 제거

GC 수행중인 스레드가 있으면, 해당 스레드를 제외한 다른 스레드는 잠시 멈춤

 

 

 

런타임 데이터 영역 (Runtime Data Area)

 

 

JVM 메모리의 영역

자바 애플리케이션 실행할 때 사용되는 데이터 적재 영역

 

 

- 모든 스레드가 공유하는 영역 >> GC 대상

힙, 메서드

 

 

- 스레드마다 하나씩 별개로 생성됨

스택, PC 레지스터, 네이티브 메서드 스택

 

 

메서드 영역 (Method Area)

클래스 멤버 변수, 데이터 타입, 접근제어자 같은 정보

메서드 정보

데이터 타입 정보

Constant Pool

정적 (Static) 변수

final class 

등이 생성되는 영역

 

 

힙 영역 (Heap Area)

new 키워드로 동적 생성되는 객체, 배열이 저장되는 영역

주기적으로 GC 의 메모리 회수 대상이 되는 영역

 

 

 

 

Young Generation

자바 객체가 생성되자마자 저장되고, 생긴지 얼마 안되는 객체가 저장되는 공간

힙에 객체 생성 시, 최초로 Eden 영역에 생성

>> 이 영역에 데이터가 누적되면 참조 정도에 따라 Servivor 의 공간으로 이동 (참조 없으면 회수)

 

Young Generation (Eden + Servivor) 영역이 가득 차게 되면 참조 정도에 따라 Tenured Generation (Old 영역)으로 이동/회수

>> GC 의 이 동작을 Minor GC 라고 부름

 

Old 영역 차면 Old 영역의 모든 객체를 검사하여 참조하지 않는 객체를 한꺼번에 삭제하는 GC 실행

>> 이 때 GC 스레드 제외한 다른 스레드 멈춤 (Stop-the_world)

Stop-the-world 이후 Old 영역의 메모리를 회수하는 GC 동작을 Major GC 라고 부름

 

 

스택 영역 (Stack Area)

지역변수, 파라메터, 리턴 값, 연산에 사용되는 임시 값 등이 생성되는 영역

 

 

PC Register

스레드 생성 될 때 마다 생성되는 영역

현재 스레드가 실행되는 부분의 주소와 명령을 저장하는 영역 (프로그램 카운터)

 

네이티브 메서드 스택 (Native Method Stack)

자바 이외의 언어로 작성된 네이티브 코드를 실행할 때 사용되는 메모리 영역 (일반적인 C스택)

보통 C, C++ 등의 코드 수행을 위한 스택을 의미

JNI 자바 컴파일러에 의해 변환된 자바 바이트코드를 읽고 해석하는 자바 인터프리터

 

 

 

'끄적 > ?' 카테고리의 다른 글

업캐스팅 다운캐스팅  (0) 2023.08.27
Generic  (0) 2023.08.27
Java ArrayList와 List 차이  (0) 2023.08.27
String, StringBuffer, StringBuilder  (0) 2023.08.27
추상클래스 인터페이스  (0) 2023.03.12

클래스 체계

가장 먼저 변수 목록

static public 상수가 맨 처음

이어서 비공개 인스턴스 변수

공개 변수가 필요한 경우는 거의 없다

 

변수 목록 다음에는 공개 함수

비공개 함수는 자신을 호출하는 공개함수 직후에 넣는다 >> 추상화 단계 순차적으로

 

 

캡슐화

변수와 유틸리티 함수는 공개하지 않는 편이 좋지만, 그렇다고 반드시 숨겨야 하는 것은 아니다

때로는 protected 선언해 테스트 코드에 접근 허용하기도 한다

같은 패키지 안에서 테스트 코드가 함수 호출, 변수 사용해야 한다면 그 함수/변수를 protected 로 선언하거나 패키지 전체로 공개한다

 

 

클래스는 작아야 한다

클래스를 만들 때 첫 번째 규칙은 크기

클래스는 작아야 한다

 

함수의 경우, 물리적인 행 수로 크기를 측정했다 

클래스의 경우는 좀 다르게, 클래스가 맡은 책임을 측정한다

 

 

클래스 이름은 해당 클래스의 책임을 기술해야 한다

Proccess, Manager, Super 등과 같이 모호한 단어가 있다면, 클래스에 여러 책임을 떠안겼다는 증거다

 

 

단일 책임 원칙

Single Responsibility Principal

클래스나 모듈을 변경할 이유가 단 하나 뿐이어야 한다는 원칙

SRP 는 책임 이라는 개념을 정의하며 적절한 클래스 크기를 제시한다

 

SRP 는 객체지향 설계에서 더욱 중요한 개념

 

>> 큰 클래스가 아닌, 작은 클래스 여러개가 더 바람직하다

작은 클래스는 각자 맡은 책임이 하나이고, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다

 

 

응집도

Cohesion

클래스는 인스턴스 변수가 작아야 한다

각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다

일반적으로 메서드가 변수를 더 많이 사용할수록 메서드, 클래스 사이의 응집도가 더 높다

(모든 메서드가 모든 인스턴스 변수를 사용하는 클래스는 응집도 최상)

 

 

public class Stack{
    
    private int topOfStack = 0;
    List<Integer> elements = new LinkedList<Integer>();
    
    pubic int size(){
        return topOfStack;
    }
    
    public void push(int element){
        topOfStack++;
        element.add(element);
    }
    
    public int pop() throws PoppedWhenEmpty{
        if(topOfStack == 0){
            throw new PoppedWhenEmpty();
        }
        int element = elements.get(--topOfStack);
        elements.remove(topOfStack);
        return element;
    }
}

Stack 클래스는 응집도가 높다

size() 함수를 제외한 다른 두 함수는 두 변수를 모두 사용한다

 

 

 

응집도를 유지하면 작은 클래스 여럿이 나온다

큰 함수를 작은 함수 여럿으로 나누기만 해도 클래스 수가 많아진다

 

 

변경하기 쉬운 클래스

대다수 시스템은 지속적인 변경이 가해진다

public class Sql {
    public Sql(String table, Column[] columns)
    public String create()
    public String insert(Object[] fields)
    public String selectAll()
    public String findByKey(String keyColumn, String keyValue)
    public String select(Column column, String pattern)
    public String select(Criteria criteria)
    public String preparedInsert()
    private String columnList(Column[] columns)
    private String valuesList(Object[] fields, final Column[] columns) 
    private String selectWithCriteria(String criteria)
    private String placeholderList(Column[] columns)
}

 

새로운 SQL 문을 지원하려면 반드시 Sql 클래스를 변경해야 한다

또한 기존 SQL 문 하나를 수정할 때도 마찬가지

ex. select 문에 내장된 select 문을 지원하려면 Sql 클래스를 고쳐야 한다

이렇듯 변경할 이유가 두 가지 이므로 Sql 클래스는 SRP 를 위반한다

 

selectWIthCriteria() 메서드는 select() 에서만 사용한다

 

 

abstract public class Sql {
    public Sql(String table, Column[] columns)
    abstract public String generate();
}
 
public class CreateSql extends Sql {
    public CreateSql(String table, Column[] columns)
    @Override public String generate()
}
 
public class SelectSql extends Sql {
    public SelectSql(String table, Column[] columns)
    @Override public String generate()
}
 
public class InsertSql extends Sql {
    public InsertSql(String table, Column[] columns, Object[] fields)
    @Override public String generate()
    private String valuesList(Object[] fields, final Column[] columns)
}
 
public class SelectWithCriteriaSql extends Sql {
    public SelectWithCriteriaSql(
    String table, Column[] columns, Criteria criteria)
    @Override public String generate()
}
 
public class SelectWithMatchSql extends Sql {
    public SelectWithMatchSql(String table, Column[] columns, Column column, String pattern)
    @Override public String generate()
}
 
public class FindByKeySql extends Sql public FindByKeySql(
    String table, Column[] columns, String keyColumn, String keyValue)
    @Override public String generate()
}
 
public class PreparedInsertSql extends Sql {
    public PreparedInsertSql(String table, Column[] columns)
    @Override public String generate() {
    private String placeholderList(Column[] columns)
}
 
public class Where {
    public Where(String criteria) 
    public String generate()
}
 
public class ColumnList {
    public ColumnList(Column[] columns) 
    public String generate()
}

 

 

 

공개 인터페이스를 각각 Sql 클래스에서 파생하는 클래스로 만들었다

valueList 같은 비공개 메서드는 해당하는 파생 클래스로 옮겼다

모든 파생 클래스가 공통으로 사용하는 비공개 메서드는 Where 과 ColumnList 라는 두 유틸리티 클래스에 넣었다

 

함수 하나를 수정한다고 다른 함수가 망가질 위험 X

테스트 관점에서 모든 논리를 구석구석 증명하기도 쉬워졌다 >> 클래스가 서로 분리되었기 때문

 

update 문을 추가할 때 기존 클래스를 변경할 필요가 전혀 없다 >> update 를 만드는 논리는 Sql 클래스에서 새 클래스 UpdateSql을 상속받아 거기에 넣으면 그만이기 때문

 

 

 

변경으로부터 격리

상세한 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다

>> 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리

 

 

Portfolio 클래스에서 TokyoStockExchange API 를 직접 호출하는 대신 StockExchange 라는 인터페이스를 생성한 후 메서드 하나를 선언한다

 

public interface StockExchange{
    Money currentPrice(String symbol);
}

다음으로 StockExchange 인터페이스를 구현하는 TokyoStockExchange 클래스를 구현

또한 Portfolio 생성자를 수정해 StockExchange 참조자를 인수로 받는다

 

public Portfolio{
    private StockExchange exchange;
    public Portfolio(StockExchange exchange){
        this.exchange = exchange;
    }
    
    ...
}

TokyoStockExchange 클래스를 흉내내는 테스트용 클래스 생성 가능

 

테스트용 클래스는 StockExchange 인터페이스를 구현하며 고정된 주가를 반환

테스트용 클래스는 단순히 미리 정해놓은 표 값만 참조

>> 전체 포트폴리오 총계가 500 불인지 확인하는 테스트 코드 작성 가능

 

 

public class PortfolioTest{
    private FixedStockExchangeStub exchange;
    private Portfolio portfolio;
    
    @Before
    protected void setUp() throws Exception{
        exchange = new FixedStockExchangeStub();
        exchange.fix("MSFT", 100);
        portfolio = new Portfolio(exchange);
    }
    
    @Test
    public void GivenFiveMSFTTotalShouldBe500() throws Exception{
        portfolio.add(5, "MSFT");
        Assert.assertEquals(500, portfolio.value());
    }
}

이처럼 테스트가 가능할 정도로 시스템 결합도를 낮추면 유연성, 재사용성 증가

결합도가 낮다 => 각 시스템 요소가 다른 요소와 변경으로부터 잘 격리되어 있다는 의미

 

결합도 줄이면 DIP (Dependency Inversion Principal) 클래스 원칙을 따르는 클래스가 자연스럽게 구현된다

** DIP 는 본질적으로 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙

 

 

 

'끄적 > Clean Code' 카테고리의 다른 글

Clean Code -7 단위테스트  (0) 2023.08.14
Clean Code -6 오류 처리  (0) 2023.08.10
Clean Code -5 객체와 자료구조  (0) 2023.08.10
Clean Code -4 형식 맞추기  (0) 2023.08.10
Clean Code -3 주석  (0) 2023.08.10

TDD 의 법칙 세 가지

 

1. 실패하는 단위 테스트를 작성할 때 까지 실제 코드를 작성하지 않는다

2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위테스트를 작성한다

3. 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다

 

 

위 규칙을 따르면 개발, 테스트가 대략 30초 주기로 묶인다

테스트 코드와 실제 코드가 함께 도출되고

테스트 코드가 실제 코드보다 불과 몇 초 전에 나온다

 

But 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다

 

 

깨끗한 테스트 코드 유지하기

실제 코드가 진화하면, 테스트 코드 역시 변해야 한다

테스트 코드가 복잡할수록 실제 코드를 짜는 시간보다 테스트 케이스를 추가하는 시간이 더 걸리게 된다

 

테스트코드는 실제 코드만큼 중요하다

 

테스트는 유연성, 유지보수성, 재사용성을 제공한다

테스트가 있으면 변경이 두렵지 않다

>> 코드에 유연성, 유지보수성, 재사용성을 제공한다

 

 

 

깨끗한 테스트 코드

깨끗한 테스트 코드를 만들기 위해서는 가독성이 중요하다

테스트 코드에서 가독성을 높이려면 명료성, 단순성, 풍부한 표현력이 필요하다

 

 

 

public void testGetPageHierarchyAsXml() throws Exception {
  makePages("PageOne", "PageOne.ChildOne", "PageTwo");

  submitRequest("root", "type:pages");

  assertResponseIsXML();
  assertResponseContains(
    "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
}

public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {
  WikiPage page = makePage("PageOne");
  makePages("PageOne.ChildOne", "PageTwo");

  addLinkTo(page, "PageTwo", "SymPage");

  submitRequest("root", "type:pages");

  assertResponseIsXML();
  assertResponseContains(
    "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
  assertResponseDoesNotContain("SymPage");
}

public void testGetDataAsXml() throws Exception {
  makePageWithContent("TestPageOne", "test page");

  submitRequest("TestPageOne", "type:data");

  assertResponseIsXML();
  assertResponseContains("test page", "<Test");
}

BUILD_OPERATE_CHECK 패턴

테스트는 정확하게 세 부분으로 나눠진다

 

첫 부분은 테스트 자료를 만든다

두 번째 부분은 테스트 자료를 조작한다

세 번째 부분은 조작한 결과가 올바른지 확인한다

 

잡다한 코드 없이 진짜 필요한 자료 유형과 함수만 사용하여 읽는 사람이 빠르게 이해할 수 있도록 구성한다

 

 

 

테스트 당 assert 하나

JUnit 으로 테스트 코드를 짤 때는 함수마다 assert 문을 단 하나만 사용한다

 

 

@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {
  hw.setTemp(WAY_TOO_COLD); 
  controller.tic(); 
  assertTrue(hw.heaterState());   
  assertTrue(hw.blowerState()); 
  assertFalse(hw.coolerState()); 
  assertFalse(hw.hiTempAlarm());       
  assertTrue(hw.loTempAlarm());
}

이 코드를 개선한게 

 

@Test
public void turnOnCoolerAndBlowerIfTooHot() throws Exception {
  tooHot();
  assertEquals("hBChl", hw.getState()); 
}
  
@Test
public void turnOnHeaterAndBlowerIfTooCold() throws Exception {
  tooCold();
  assertEquals("HBchl", hw.getState()); 
}

@Test
public void turnOnHiTempAlarmAtThreshold() throws Exception {
  wayTooHot();
  assertEquals("hBCHl", hw.getState()); 
}

@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
  wayTooCold();
  assertEquals("HBchL", hw.getState()); 
}

이 코드이다

하나의 테스트 당 하나의 assert 를 사용하여 한 눈에 알아보고 이해하기 쉽게 구현했다

But 리턴 값을 전부 비교해야 하는 테스트는 어쩔 수 없이 여러개의 assert 문을 사용해야된다

 

 

테스트 당 개념 하나

테스트 함수 마다 한 개념만 테스트 하라

 

 

F.I.R.S.T

깨끗한 테스트는 다음 다섯 규칙을 따른다

 

1. 빠르게 (Fast)

테스트는 빨라야 한다

테스트가 느리면 자주 돌릴 엄두를 못낸다

 

2. 독립적으로 (Independent)

각 테스트는 서로 의존하면 안된다

한 테스트가 다음 테스트가 실행 될 환경을 준비해서는 안된다

각 테스트는 독립적으로, 어떤 순서로 실행해도 괜찮아야 한다

 

3. 반복 가능하게 (Repeatable)

테스트는 어떤 환경에서도 반복 가능해야 한다

실제 환경, QA환경, 클라우드 환경 등 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 둘러댈 변명이 생긴다

 

4. 자가 검증하는 (Self_validating)

테스트는 bool 값으로 결과를 내야 한다

성공 아니면 실패

통과 여부를 알기 위해 로그를 읽어서는 안된다

 

 

5. 적시에 (Timely)

테스트는 적시에 작성해야 된다

단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다

실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사싱르 발견할지도 모른다

 

 

 

 

마지막으로

테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화하기 때문에 지속적으로 깨끗하게 관리해야 한다

표현력을 높이고 간결하게 정리하자

테스트 API를 구현해 도메인 특화 언어를 만들자 (DSL) >> 그러면 그만큼 테스트 코드 짜기 쉬워진다

 

'끄적 > Clean Code' 카테고리의 다른 글

Clean Code -8 클래스  (0) 2023.08.14
Clean Code -6 오류 처리  (0) 2023.08.10
Clean Code -5 객체와 자료구조  (0) 2023.08.10
Clean Code -4 형식 맞추기  (0) 2023.08.10
Clean Code -3 주석  (0) 2023.08.10

 

오류 코드보다 예외를 사용하라

public class DeviceController {
    
    public void sendShutDown(){
        DeviceHandle = getHandle(DEV1);
        if(handle != DeviceHandle.INVALID){
            ...
        }
        else if
        ...
    }
}

이같은 방법으로 오류를 검출하고 그에 대한 동작을 부여하면 코드가 복잡해진다

차라리 오류 발생 시 예외를 던지는 것이 더 좋다 >> 호출자 코드가 더 깔끔해지기 때문

 

 

public class DeviceController{
    ...
    public void sendShutDown(){
        try{
            tryToShutDown();
        }
        catch(DeviceShutDownForError e){
            logger.log(e);
        }
    }
    
    private void tryToShutDown() throws DevieSHutDownError{
        ...
    }
}

개선한 코드

디바이스를 종료하는 알고리즘과 오류를 처리하는 알고리즘을 분리했기 때문에 가독성 증가

 

 

Try-Catch-Finally 문 부터 작성하라

try catch 문에서 catch 문은 try 블록에서 무슨 일이 생겨도 프로그램 상태를 일관성 있게 유지해야 한다

 

    public List<Object> errorCheck(String name){
        return new ArrayList<Object>();
    }

    public List<Object> errorCheck(String name){

        try{
            ...
        }
        catch(Exception e){
            throw new Exception(e);
        }
        
        return new ArrayList<Object>();
    }

위의 errorCheck 메서드에서는 ArrayList 를 바로 리턴해준다

만약 에러가 발생하면 예외를 던지지 못하므로 단위 테스트는 실패하게 된다

 

아래에서는 단위테스트를 실패할 때 에러를 던지도록 catch 문을 구현했다

코드가 예외를 던지므로 테스트 자체는 성공한다

 

try catch 문으로 범위를 정의했으므로 TDD를 사용해 필요한 논리를 추가가 가능

범위 내에서 트랜잭션의 본질을 유지하기 좋아진다

 

 

미확인 (Unchecked) 예외를 사용하라

메서드가 반환할 예외를 하나하나 열거하며 예외 처리를 하는 방법은 좋지 않다

 

 

예외에 의미를 제공하라

예외를 던지는 경우에는 전후 상황을 충분히 덧붙여라

오류 발생 원인, 위치를 찾기 쉬워지기 때문

 

오류 메세지에 정보를 담아 예외와 함께 throw

실패한 연산 이름과 실패 유형도 언급

logging 기능 사용한다면 catch 블록에서 오류 기록

 

 

호출자를 고려해 예외 클래스를 정의하라

Exception 클래스를 만드는데 가장 중요한 것은 '오류를 잡아내는 방식' 이다

라이브러리 API 디자인에 종속적이지 않게 설계할 수 있다

 

    ACMEPort port = new ACMEPort(12);
    
    try{
        port.open();
    }
    catch(DeviceResponseException e){
        reportPortError(e);
        logger.log("unlock Exception", e);
    }
    catch(GMXError e){
        reportPortError(e);
        logger.log("Device response exception");
    }
    finally{
        ...
    }

오류 검출에 대한 중복이 심하지만, 예외에 대응하는 방식이 예외 유형과 무관하게 동일하다

 

    LocalPort port = new LocalPort(12);
    try{
        port.open();
    }
    catch(PortDeviceFailure e){
        reportError(e);
        logger.log(e.getMessage(), e);
    }
    finally{
        ...
    }

따라서 이와 같이

호출하는 라이브러리 API 를 감싸면서 예외 유형 하나를 반환하도록 고칠 수 있다

 

여기서 LocalPort 클래스는 단순히 ACMEPort 클래스가 던지는 예외를 잡아 변환하는 wrapper 클래스일 뿐이다

 

실제 외부 API를 사용할 때에는 감싸기 기법이 좋다

외부 API를 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다

 

 

 

정상 흐름을 정의하라

try{
        MealExpense expenses = expenseReportDAO.getMeals(employee.getID());
        m_total += expenses.getTotal();
        
    }
    catch(MealExpensesNotFound e){
        m_total += getmealPerDiem();
    }

위에서 식비를 비용으로 청구했다면, 직원이 청구한 식비를 총계에 더한다

식비를 비용으로 청구하지 않았다면, 일일 기본 식비를 총계에 더한다

 

>> 예외가 논리를 따라가기 어렵게 만든다

특수한 상황 처리할 필요가 없다

 

    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();

예외 처리를 하지 않고 위 코드로만 동작하도록 ExpenseReportDAO 를 수정

 

public class PerDiemMealExpenses implements MealExpenses{
    public int getTotal(){
        // 기본값으로 일일 식비를 반환한다
    }
}

getTotal() 메서드는 언제나 MealExpense 객체를 반환한다

청구한 식비가 없다면 일일 기본 식비를 반환한다

>> 청구된 식비 여부에 따른 예외 처리가 필요 없다

 

이를 특수사례 패턴이라고 한다

클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식

 

 

null 을 반환하지 마라

 

public void registerItem(Item item){
    if(item != null){
        ItemRegistry registry = peristentStore.getItemRegistry();
        if(registry != null){
            Item existing = registry.getItem(item.getID());
            if(existing.getBillingPeriod().hasRetailOwner()){
                existing.register(item);
            }
        }
    }
}

 

 

한줄 건너 하나씩 null 을 체크하는 좋지 않은 코드의 예

 

null 을 반환하는 코드는 호출자에게 문제를 떠넘기게 된다

하나라도 null 확인을 빼먹는다면 애플리케이션이 통제를 벗어날 수 있다

 

위 코드에선 두 번째 행에 null 확인이 빠졌다

if(persistentStore == null) 조건이 true 라면 실행 시 NullPointException 이 발생

>> 발생 된 Exception 을 처리할 코드가 없다

 

메서드에서 null 을 반환하고싶은 유혹이 든다면, 대신 예외를 던지거나 특수 사례 객체를 반환하라

 

 

List<Employee> employees = getEmployees();
if(employee != null){
    for(Exployee e : employee){
        totalPay += e.getPay();
    }
}

 

위에서 getEmployees 는 null 도 반환한다

하지만 반드시 null 을 반환할 필요가 없으니까

 

    List<Employee> employees = getEmployees();
    for(Exployee e : employee){
        totalPay += e.getPay();
    }
    
    
    public List<Employee> getEmployees(){
        if( noEmployee ){
            return Collection.emptyList();
        }
    }

이렇게 고칠 수 있겠다

 

getEmployees() 는 언제나 리스트를 반환한다

Employee 가 없을 경우에는 미리 정의된 읽기 전용 리스트를 반환한다

 

이렇게 하면 예외 처리 필요 없이 객체를 무조건 반환 받는 방식으로 코드 작성이 가능하다

 

 

 

null 을 전달하지 마라

public class MetricsCalculator {
    public double xProjection(Point p1, Point p2){
        return (p2.x - p1.x) * 1.5;
    }

    ...
}

이같은 메소드에

 

calculator.xProjection(null, new Point(12, 13));

이렇게 null 을 던지면 NullPointException 발생

 

if(p1 == null || p2 == null) throw InvalidArgumentException("Exception message");

이렇게 if 문을 사용할 수 있지만, if 문은 InvalidArgumentException 을 잡아내는 처리기가 별도로 필요하다

 

public class MetricsCalculator {
    public double xProjection(Point p1, Point p2){
        assert p1 != null : "p1 should not be null";
        assert p2 != null : "p2 should not be null";
        return (p2.x - p1.x) * 1.5;
    }

    ...
}

assert 문을 사용하는 방법 역시 존재하나, 누군가가 null 을 전달하면 여전히 실행 오류가 발생

 

>> 애초에 null 이 안넘어오도록 코드 짜라

 

 

 

'끄적 > Clean Code' 카테고리의 다른 글

Clean Code -8 클래스  (0) 2023.08.14
Clean Code -7 단위테스트  (0) 2023.08.14
Clean Code -5 객체와 자료구조  (0) 2023.08.10
Clean Code -4 형식 맞추기  (0) 2023.08.10
Clean Code -3 주석  (0) 2023.08.10

+ Recent posts