본문 바로가기

Clean Code

클래스를 잘 설계하기 #9

목차

  • 캡슐화되어야 한다.
  • 단일 책임 원칙
  • 낮은 결합도, 높은 응집도
  • 변경하기 쉬워야 한다.

캡슐화되어야 한다.

객체의 실제 구현을 오부로부터 감추는 방식

  • 클래스를 개발할 때 기본적으로 구현을 감추고, 외부 객체와 상호작용하는 부분만 노출한다.
  • 외부의 잘못된 사용을 방지한다.
  • 경계에서 배웠던 부분! Map

 

 

Stack 예제

  • 필드를 private로 제한, get 으로 읽기
  • 수정은 push, pop 메서드를 통해서 일어나도록 제한
class Stack<T> {
    class Node<T> {
        private T data;
        private Node<T> prev;

        Node(T data) {
            this.data = data;
        }
    }

    private Node<T> top;

    public void push(T data) {
        Node<T> node = new Node<T>(data);
        node.prev = top;
        top = node;
    }

    public T pop() {
        if(top == null) {
            throw new NoSuchElementException();
        }
        T data = top.data;
        top = top.prev;

        return data;
    }

    public T peek() {
        if(top == null) {
            throw new NoSuchElementException();
        }

        return top.data;
    }

    public boolean isEmpty() {
        return top == null;
    }
}

단일 책임 원칙

클래스는 작아야 한다. 

// 두 가지 책임을 가지는 클래스
public class SuperDashBoard extends JFrame implements MetDataUser {
    // focus
    public Component getLastFocusedCompnent();
    public void setLastFocused(Component lastForcused);
    
    // version
    public int getMajarVersionNumber();
    public int getMinorVersionNumber();
    public int getBuildNumber();
}
  • 함수와 마찬가지로 클래스도 작아야 한다.
  • 함수는 라인 수로 크기를 측정했는데, 클래스는 맡은 책임의 수로 크기를 측정한다.
  • 클래스 설명은 만일(if), 그리고(and), 하지만(but)을 사용하지 않고 25단어 내외로 가능해야한다. -> 책임이 한 가지여만 한다.

클래스가 맡은 책임이 한 개인가

// 클래스를 분리하여 사용
public class SuperDashBoard extends JFrame implements MetDataUser {
    public Component getLastFocusedCompnent();
    public void setLastFocused(Component lastForcused);
}

// 다른 곳에서도 재활용할 수 있다.
public class version {
    public int getMajarVersionNumber();
    public int getMinorVersionNumber();
    public int getBuildNumber();
}

 

  •  SPR 해야한다. 자잘한 단일 클래스가 많아지면 큰 그림을 이해하기 어렵다고 우려한다. 하지만 작은 클래스가 많은 시스템이든 큰 클래스가 몇 개뿐인 시스템이든 돌아가는 부품은 그 수가 비슷한다.
  • "도구상자를 어떻게 관리하고 싶은가? 작은 서랍을 많이 두고 기능과 이름이 명확한 컴포넌트를 나눠 넣고 싶은가? 아니면 큰 서랍 몇개를 두고 모두를 던져 넣고 싶은가?"
  • 큼직한 다목적 클래스 몇 개로 이뤄진 시스템은 (변경을 가할 때) 당장 알 필요가 없는 사실까지 들이밀어 독자를 방해한다.
  • 작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.

낮은 결합도, 높은 응집도

결합도 : 다른 모듈간의 의존도

응집도 : 모듈 내부의 기능 응집도

결합도는 낮을수록 응집도는 높을 수록 유지보수성이 좋다.

결합도가 높은 클래스이 문제점

  • 연관된 클래스가 변경되면 수정이 필요하다.
  • 결합도가 높으면 연관된 클래스들을 모두 이해해야 한다.

응집도가 낮은 클래스의 문제점

  • 여러 기능이 있으므로 이해하기 어렵다.
  • 재사용하기 어렵다.

낮은 결합도

  • 시스템의 결합도를 낮추면 유연성과 재사용성도 더욱 높아진다.
  • DIP - 클래스가 상세한 구현이 아니라 추상화에 의존해야한다.
  • 추상화를 이용하면 테스트 코드 짜기에 용이하다.
public class TokyoStockExchange {
    public Money currentPrice(String symbol);
}

// Portfolio 클래스는 TokyoStockExchange에 의존하고 있어 높은 결합도를 가지고 있다.
public Portfolio {
    private TokyoStockExchange tokyoStockExchange;
    public Portfolio(TokyoStockExchange change) {
    	this.tokyoStockExchange = change;
    }
}

 

// 인터페이스를 통해 Portolio와 TokyoStockExchange의 결합도는 끊어준다. 
public interface StockExchange {
    Money currentPrice(String symbol);
}

public class TokyoStockExchange implements StockExchange {
    public Money currentPrice(String symbol) {
    	// call API..
    }
}

public Portolio {
    private StockExchange exchange;
    public Portolio(StockExchange exchange) {
    	this.exchange = exchange;
    }
}
  • 그러나 확장될 가능성이 적다면 일단은 결합하고, 나중에 추상화해도 된다.

높은 응집도

  • 클래스는 인스턴스 변수 수가 적어야 한다. 메서드는 인스턴스 변수를 하나 이상 사용해야 한다. 메서드가 인스턴스가 변수를 많이 사용할수록 응집도가 높다.
  • 응집도가 높다 = 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다 = 서로 관계있는 애들만 모여있다.
  • 클래스가 응집도를 잃어간다면 함수를 쪼개야 한다. => 다른 클래스도 분리

변경하기 쉬워야 한다.

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 valueList(Object[] fields, final Column[] columns);
    private String selectWithCriteria(String criteria);
    private String placeholderList(Column[] columns);
}
  • update문을 추가해 한다면?
  • 새로운 SQL을 추가할 때도 수정이 발생하고, 기존 SQL문을 수정할 때도 수정이 발생하므로 OOP 위반된다.
abstract public class Sql {
    public Sql(String table, Column[] columns)
    abstract public String genreate();
}

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 Where {
    public Where(String criteria)
    public String generate()
}

public class ColumnList {
    public ColumnList(Column[] columns) 
    public String generate()
}
  • 공개 인터페이스를 전부 SQL 클래스에서 파생하는 클래스로 만들고, 비공개 메서드는 해당 클래스로 옮기고, 공통된 인터페이스는 따로 클래스로 뺐다
  • 기존의 클래스를 건드리지 않아도 된다.

=> update문을 추가하더라도 새로운 클래스를 만들어 추가해주면 된다. 기존코드를 안건드려도 된다.

 

하지만 만약 table에 컬럼이 추가되거나 삭제된다며..?  => JPA를 사용하자!

'Clean Code' 카테고리의 다른 글

창발적 설계로 깔끔한 코드 구현하기 #11  (0) 2021.08.17
관심사 분리 패턴 #10  (0) 2021.08.17
단위 테스트 #8  (0) 2021.08.09
모호한 경계를 구분짓기 #7  (0) 2021.08.05
예외 처리하기 #6  (0) 2021.08.04