본문 바로가기

FrontEnd

자바로 알아보는 결합도(커플링)

반응형

https://www.linkedin.com/pulse/types-coupling-ahmed-adel/

 

Types of Coupling

As our Software grows, and the communication between it's Modules gets more complex, we come to the point where "Coupling" matters, coupling occurs when there are inter-dependencies between one module and another, the tighter this coupling is, changes in o

www.linkedin.com

결합도

소프트웨어가 성장하고 모듈 간의 통신이 더 복잡해짐에 따라 "결합(커플링)"이 중요한 지점에 도달합니다.
결합은 한 모듈과 다른 모듈 사이에 상호 종속성이 있을 때 발생합니다.
이 결합이 더 단단할수록 한 곳의 변경이 다른 곳의 변경에 영향을 미칩니다.
커플링이 느슨한 경우 한 위치의 변경이 다른 위치에 영향을 주어서는 안 됩니다.
원하는 상태는 가능한 가장 느슨한 연결입니다.
이 기사에서는 커플링 유형을 살펴보겠습니다.
이 것들은 소프트웨어를 구축할 때 피해야 합니다.
 
 

들어가기 앞서

  • 스프링은 거대한 팩토리 메서드에 불과하다.
    • 객체 생성과 종속성 주입을 통한 객체 관계 그래프 생성을 대신해서 거대한 결합도를 대신 처리해준다.
    • new 키워드는 강결합이다.
  • 스프링이 없는 프론트엔드는 객체 생성과 종속성 주입을 개발자가 직접 구현해야 한다
    • contextAPI의 컨텍스트 / 컨테이너 패턴의 컨테이너
      • 여기서 수 많은 잡다한 의존성을 처리한다.
      • 해당 패턴은 결합도 처리의 핵심이다.
    • 프리젠테이션 컴포넌트, 혹은 useContext 훅을 사용하는 컨슈머 컴포넌트는 컨텍스트 / 컨테이너의 존재에 상관없이 인터페이스 기반으로 api(프롭, 콜백함수, 훅)를 컨슘한다.

1- Content Coupling

이 결합은 한 클래스가 다른 클래스의 내부 상태(예: 변수)를 수정할 때 발생합니다.

이 결합에 대한 해결책은 내부 상태 멤버를 "프라이빗"으로 선언하는 것이며

퍼블릭 메서드를 통하지 않고는 업데이트되지 않아야 합니다.

 

프라이빗 변수에 "getters" 및 "setters"를 제공할 수 있지만(이 자체는 나쁘지 않음),

이 변수에 대한 계산이 다른 클래스에서 수행는 경우 콘텐츠 결합을 방지하지 못할 수 있습니다.

예를 들면 다음과 같습니다.

// tight coupling :

public int sumValues(Calculator c){
    int result = c.getFirstNumber() + c.getSecondNumber();
    c.setResult(result);
    return c.getResult();
}

// loose coupling :

public int sumValues(Calculator c){
    c.sumAndUpdateResult();
    return c.getResult();
}
첫 번째 예에서는 Calculator 클래스의 내부 상태가 private이지만
다른 객체의  메서드에서 계산을 수행하고 결과를 Calculator에 다시 설정합니다.
이 경우 setter와 getter의 주인은 자신의 상태를 스스로 책임지지 못하고 있습니다.
 
두 번째 예에서는 Calculator에 "sumAndUpdateResult()"를 요청하므로
Calculator는 자체 내부 상태 업데이트를 완전히 제어할 수 있습니다.
그리고 우리는 getResult()를 통해 업데이트 결과를 요청합니다. 
  • new 생성자 호출 / 객체 리터럴 생성도 위의 결합도를 생성합니다.
    • 팩터리 메서드를 통해 강결합을 한 곳으로 몰아야합니다.
    • 이걸 해주는게 스프링입니다.

2- Common Coupling

이 결합은 "전역 변수"를 선언할 때 발생합니다.
이 변수를 사용하는 모든 클래스가 서로 결합되고 이 변수의 변경 사항이 결합된 모든 클래스에 영향을 미치기 때문입니다.
시스템 전체에서 전역 상수를 허용하는 경우도 있찌만,
그러한 행동조차도 해당 상수에 의존하는 결합된 클래스 집합을 만들고
해당 상수 또는 해당 상수가 선언된 클래스에 대한 리팩토링은 클래스도 영향을 받습니다.
 
해결책은 "Singleton" 패턴입니다.
여기서 Singleton 클래스는 내부 상태를 변경하는 메서드와 다른 클래스가
이 싱글톤 클래스의 상태에 액세스할 수 있는 getter를 보유합니다.
싱글톤 클래스는 변경의 영향을 받지만 다른 클래스는 여전히 동일한 메서드를 사용합니다.
 
  • 공유 데이터 관리 클래스를 만들어, 해당 데이터 변경에 대한 책임을 자신이 지게 합니다.
  • 단순 조회만 하는것은 문제가 안됩니다.

 

Android 애플리케이션의 또 다른 솔루션은 리소스 XML 파일을 사용하고 "R" 클래스를 통해 해당 상수에 액세스하는 것입니다.

이 클래스는 프로젝트에서 제거되거나 이름이 바뀌지 않기 때문입니다.

  • 대체적으로 커먼 커플링 까지를 매우 높은 결합도로 취급하는것 같습니다.
  • 해당 커플링을 싱글턴, 종속성 주입을 통해 추상화 하는것이 팩터리 패턴입니다.

여긴 설명만으로 이해가 잘 안되는것 같아 자바스크립트 코드를 첨부합니다.

var Global = "global";
function A() {
  Global = "A";
}
function B() {
  Global = "B";
}

 

3- Control Coupling

이 커플링은 한 함수가 "매개변수"를 사용하여 다른 함수를 호출하고

이 "매개변수"가 호출된 함수가 특정 작업을 트리거하도록 강제할 때 발생합니다.

이로 인해 두 번째 함수가 첫 번째 함수에 의해 제어됩니다.

 switch-case 및 if-else 블록이 발생하는 곳에서 찾을 수 있습니다.

 

이러한 결합의 결과는 호출자와 호출된 함수 모두에 영향을 주는 변경을 야기할 것입니다.

컨트롤 결합을 제거하기 위해 다형성 기술을 사용할 수 있습니다.

// tight coupling :

public void run(){
    takeAction(1);
}

public void takeAction(int key) {
    switch (key) {
        case 1:
            System.out.println("ONE RECEIVED");
            break;
        case 2:
            System.out.println("TWO RECEIVED");
            break;        
    }
}

// loose coupling :

public void run(){
    Printable printable = new PrinterOne();
    takeAction(printable);
}

public void takeAction(Printable printable){
    printable.print();
}

public interface Printable{
    void print();
}

public class PrinterOne implements Printable{
    @Override
    public void print() {
        System.out.println("ONE RECEIVED");
    }
}

public class PrinterTwo implements Printable{
    @Override
    public void print() {
        System.out.println("TWO RECEIVED");
    }
}​

(주 : 어차피 팩터리에서 플래그 받아서 처리해야 할텐데... 이런 식으로)

var TableClothFactory = {
  getTableCloth: function(color) {
    return Object.create(TableCloth, { color: { value: color } });
  }
};​
어떤 상황에서는 이것이 불가능하므로 테이블 조회 기술을 사용하여 제어 결합을 줄일 수 있습니다.
js는 map혹은 객체 리터럴 활용
이 기술을 구현하는 라이브러리는 CommandsMap입니다( https://github.com/Ahmed-Adel-Ismail/CommandsMap ) :
public static void main(String ... args){
    new Printer().takeAction(1);
}


@CommandsMapFactory
public class Printer {

    private final CommandsMap map = CommandsMap.of(this);

    public void takeAction(int key){
        map.execute(key);
    }

    @Command(1)
    void printOne() {
        System.out.println("ONE RECEIVED");
    }

    @Command(2)
    void printTwo() {
        System.out.println("TWO RECEIVED");
    }
}

4- Stamp Coupling

이 결합은 한 클래스가 다른 클래스의 함수 매개변수의 타입으로 선언될 때 발생합니다.
이 경우 이 다른 클래스는 첫 번째 클래스와 밀접하게 결합되며
첫 번째 클래스의 변경 사항은 다른 클래스의 함수 구현에 영향을 미칩니다.
또한 이러한 결합의 또 다른 단점은 이 함수를 사용할 때마다 사용자가 매개변수의 타입인 첫 번째 클래스에 의존해야 한다는 것입니다.
예를 들면 다음과 같습니다.
public class NumberGenerator {
    public Integer generate() {
        return (int) (Math.random() * 100);
    }
}

public class Printer {

    // stamp coupling - happens when using another class as the type
    // of the method parameter 
    public void printNumber(NumberGenerator generator) {
        System.out.println(generator.generate());
    }
}

public class PrinterWrapper {

    private final Printer printer;

    public PrinterWrapper(Printer printer) {
        this.printer = printer;
    }

    // we must depende on NumberGenerator.java although this class 
    // is concerned with Printer.java Wrapping ... this class is now 
    // coupled with NumberGenerator.java due to the method
    // parameter type in Printer.java
    public void print(NumberGenerator generator) { 
        printer.printNumber(generator);
    }
}
스탬프 커플링을 피하는 한 가지 방법은 가능한 경우 타입 대신 프리미티브를 사용하는 것입니다.
public class Printer {
    public void printNumber(int generatedNumber) {
        System.out.println(generatedNumber);
    }
}
어떤 경우에는 메소드 매개변수를 프리미티브로 변환한 후 필수 매개변수가
3개의 매개변수를 초과하여 "데이터 커플링"(후에 설명함)을 유발할 수 있습니다.
이 경우 이 함수만을 위해 간단한 데이터 구조(객체)를 생성할 수 있습니다. 
public class EmailSender {

    public void send(SendParams params) {
        // ...
    }

    // encapsulation in such kind of Objects is not neccessary
    // since they are not actual objects, they are a data structure 
    // that is used only once before passing it to the send() method
    public static class SendParams {
        public String email;
        public String title;
        public String signature;
        public String content;
    }

}​

또 다른 방법은 인터페이스를 사용하는 것입니다. 여기서 인터페이스는 구체적인 구현이 아니라 추상적인 계약입니다.

(콜백과 유사한 개념)

public class NumberGenerator implements Callable<Integer>{
    public Integer call() {
        return (int) (Math.random() * 100);
    }
}

public class Printer {
    public void printNumber(Callable<Integer> numberGenerator) {
        System.out.println(numberGenerator.call());
    }
}

public class PrinterWrapper {

    private final Printer printer;

    public PrinterWrapper(Printer printer) {
        this.printer = printer;
    }

    public void print(Callable<Integer> numberGenerator) { 
        printer.printNumber(numberGenerator);
    }
}​

5- Data Coupling

이 결합은 함수에 너무 많은 매개변수가 있을 때 발생합니다.
이러한 결합의 단점은 함수의 호출자가 중요하지 않은 인수를 포함하여 모든 인수를 전달해야 한다는 것입니다.
호출하는 동안 함수에 "null"을 전달하거나 함수가 허용 가능한 매개변수 수를 초과할 때 이러한 결합을 발견할 수 있습니다. 
 
(밥 삼촌의 책 "Clean Code"에 따르면 세 개의 매개변수는 많은 것으로 간주됨)
데이터 커플링과 스탬프 커플링 사이에는 항상 절충점이 있습니다.
여기서 하나를 사용하면 다른 하나가 제거되지만
가장 좋은 경우는 하나 또는 두 개의 간단한 매개변수를 사용하는 함수를 선언하는 것입니다.
필요한 경우 함수를 분리해야 할 수도 있습니다.
즉, 함수를 여러 함수로 만들거나
이 함수를 위해 특별히 만든 작은 데이터 구조(객체)를 사용할 수 있습니다.
하지만 특별한 자료구조를 사용하는 경우는 스탬프 커플링을 줄이지만 제거하지는 않습니다.

6- Routine Call Coupling

이 결합은 루틴/메서드/함수가 다른 것을 호출할 때 발생하며

이 결합은 모든 시스템에 존재합니다.

이 결합의 단점은 호출된 함수를 변경하면 호출자에게 영향을 미친다는 것입니다.
두 개 이상의 함수가 항상 서로를 호출하는 경우
해당 함수를 하나의 함수로 그룹화하고
예를 들어 "프록시 패턴"을 사용하는 것과 같이 이 새로운 함수를 처리할 수 있습니다.
이러한 커플링의 영향을 줄이기 위해
"이벤트 기반 아키텍처",
"발행 - 구독 패턴(Pub-Sub)" 또는
"메시지 기반 아키텍처"를 사용할 수 있지만
이러한 경우 "컨트롤 커플링"을 처리해야 합니다.
상황과 시스템에 어느 것이 더 잘 맞는지에 따라 절충합니다.

7- Type Use Coupling

이 커플링은 모듈(클래스)이 다른 하나에 멤버 변수 또는 로컬 변수로 종속될 때 발생하며
이 커플링의 단점은 다른 클래스가 변경되면 해당 클래스가 이러한 변경의 영향을 받을 수 있다는 것입니다.
이러한 결합의 영향을 줄이려면 변수를 가능한 가장 제네릭(일반적인)한 타입으로 선언하십시오.
예를 들면 다음과 같습니다.
public class NumberGenerator implements Callable<Integer> {
    public Integer call() {
        return (int) (Math.random() * 100);
    }
}

// tight coupling :

public class Printer {

    // depends on a specific implementation
    private NumberGenerator generator;

    public Printer(NumberGenerator generator) {
        this.generator = generator;
    }

    public void printNumber() {
        System.out.println(generator.call());
    }
}

// loose coupling :

public class Printer {

    // depends on an interface
    private Callable<Integer> generator;

    public Printer(Callable<Integer> generator) {
        this.generator = generator;
    }

    public void printNumber() {
        System.out.println(generator.call());
    }
}
이것이 "Dependency Inversion" 원리의 핵심이며, 이러한 결합을 방지하는 데 도움이 됩니다.
  • 구체적인것이 추상적인 것에 의존하도록 하라
    • 구체 클래스를 추상적인 인터페이스를 구현하도록 하여, 사용하는 곳에서 추상 인터페이스만 사용할 수 있게 함

8- Import Coupling

이 결합은 "가져오기" 문에서 전체 패키지를 가져올 때 발생합니다.
최신 IDE는 이러한 동작을 비활성화하지만 이러한 가져오기를 수행하면 몇 가지 단점이 있습니다.

가져온 패키지가 변경된다고 가정해봅시다.

변경된 패키지가 기존의 다른 import된 클래스와 충돌하는 이름으로 클래스를 선언하여

기존 클래스에 정규화된 이름을 사용하는 것과 같이 import된 클래스도 강제로 변경될 수 있습니다.

솔루션은 간단합니다. 필요한 클래스만 가져오기만 하면 됩니다.

9- External Coupling

이 커플링은 외부 라이브러리 또는 시스템에 대한 종속성이 있을 때 발생합니다.
이러한 종속성이 존재하는 코드의 위치 수를 줄이는 것이 가장 좋습니다.
또한 코드와 이 외부 라이브러리 사이에 레이어를 생성하면 이러한 유형의 커플링이 감소할 수 있습니다. 
"파사드 패턴"과 "리포지토리 패턴"이 도움이 될 수 있습니다.
  • 이것도 써먹으려면 팩토리 필요함
* 이 글은 본 영상(https://youtu.be/FMKv8Vozf5c?t=58m36s)과 본 영상(https://youtu.be/XQnytAeZrWE)의 내용을 바탕으로 작성되었습니다.

더 볼만한 자료

https://ui.toast.com/weekly-pick/ko_20150522

 

Javascript의 커플링 측정

이 글은 커플링을 Javascript기반 예제를 통해 설명한다. 먼저 커플링이란 서로 다른 객체 또는 모듈간의 관계를 뜻한다. 그리고 그 관계의 방법은 조금씩은 다르지만 거의 유사한 패턴이므로 측정

ui.toast.com

new, 객체 리터럴 생성은 강결합.

해당 객체 사용을 (추상) 팩터리 메서드로 대체

반응형