본문 바로가기
개발관련/Spring(BE)

Spring Boot - 강한 결합(Tight Coupling)과 느슨한 결합(Loose Coupling)

by yjoo_ 2023. 11. 1.

오늘은 간단하게 인텔리제이 사용법도 익히면서 자바 개념을 익혀보자

 

먼저 저번 포스팅 때 만든 프로젝트를 열어준다.

 

처음 시작하면 기본적인 main 클래스와 패키지가 보일 것이다.

패키지는 냅두고 클래스는 삭제해준다.

 

지금 사용할 것이 아니라서 큰 의미가 없다.

 

패키지를 마우스로 잡고 우클릭이나 Alt + Insert로 새 클래스를 만들어 줄 수 있다.

 

자바 클래스를 새로 생성해주자. 이름은 App01MealBasicJava로 하겠다.

이제 우리가 작성한 코드를 실행하는 main 메서드가 있어야한다.

 

인텔리제이에서 main 메서들을 정의하는 법은 단축어로 쉽게 가능하다.

psvm을 입력하면 된다.

자 이제 코드를 작성해보도록 하겠다.

package com.learnspringframework.learnspringframework;

public class App01MealBasicJava {
    public static void main(String[] args) {
        var carrot = new Carrot();
        var meal = new Meal(carrot);
        meal.eat();
    }
}

 

var? 타입 추론

여기서 변수의 타입을 var라고 작성해주었는데, 이 문법은 타입 추론이라고 한다.

 

개발자가 타입을 명시해주지 않아도 컴파일러가 뒤에 대입된 리터럴로 타입을 추론해주는 것이다.

 

물론 이러한 문법은 초기화 한 지역 변수만 가능하다는 특징이 있다.

 

자바를 왠만큼 해본 사람이라면 알겠지만 우린 Carrot과 Meal 클래스 선언없이 인스턴스를 생성했다.

 

사진처럼 빨간색 라인이 떴을 텐데 저 위치에서 Alt+Enter를 입력하면

문제 해결을 위한 솔루션을 인텔리제이에서 제공해준다.

 

클래스 생성을 선택하면 새로운 창이 뜨게 된다.

 

여기서 그냥 추가하지 말고 맨 뒤에 .food를 붙여서 추가해보자

우리 패키지 밑에 새로운 디렉토리가 생성되고, 새로운 클래스가 디렉토리 안에 생성된다.

 

마찬가지로 Meal 클래스도 만들어보자.

 

Meal 클래스는 아무것도 없는 텅 빈 Carrot 클래스와 다르게 하나가 더 선언되었다.

 

우리는 이걸 클래스 생성자라고 한다.

 

클래스 생성자엔 자동으로 Carrot 클래스를 인자로 받도록 선언되어있다.

 

왜 이게 자동으로 되는걸까?

main 클래스로 돌아가서 살펴보면 우리는 Meal 인스턴스를 생성할 때 carrot을 인자로 줬다.

 

meal이 생성될 때는 반드시 carrot을 인자로 받아야 한다는 걸 여기서 선언한 셈이나 다름없다.

 

인텔리제이에서 이런 부분들을 캐치해서 개발자에게 편리함을 제공한다.

 

생성자(constructor)

생성자는 클래스를 new 연산자를 통해 인스턴스를 생성할 때 반드시 호출되는 일종의 메소드다.

(메소드와는 다르게 취급한다)

선언 방법

선언 방법은 클래스명과 동일하게 작성해주면 된다.

 

이 때 매개변수는 개발자의 필요에 따라 달라지게 된다.

 

생성자는 클래스마다 있을 필요가 없으나, 매개변수를 받게되는 경우 선언하지 않으면 에러가 발생하게 된다.

 

외부에서 받은 carrot인스턴스를 내부 변수에 저장해보도록 하겠다.

 

Meal 클래스를 다음과 같이 작성한다.

package com.learnspringframework.learnspringframework.food;

public class Meal {
	// 이렇게 Carrot 클래스에 의존해서 작동하게 만든 것을
	// 의존성 주입(DI)이라 부르게된다.
    Carrot carrot;
    public Meal(Carrot carrot) {
        this.carrot = carrot;
    }
}
이렇게 Carrot 클래스를 타 클래스 내부에서 사용하게 만든 것을 의존성 주입(DI)이라고 한다
의존성을 주입하게 되면 프로그램을 조립식으로 구성하기 용이하기 때문에
부품만 갈아끼우면 되는 유연한 프로그램을 만들 수 있다. 

this 키워드는 자바에서 객체 자기자신을 가르킨다.

해당 키워드를 활용해 매개변수로 받은 carrot과 멤버변수 carrot를 구분할 수 있다.

 

마지막으로 main에 남은 오류를 해결해보자. 마찬가지로 Alt+Enter로 해결할 수 있다.

 

package com.learnspringframework.learnspringframework.food;

public class Meal {
    Carrot carrot;
    public Meal(Carrot carrot) {
        this.carrot = carrot;
    }

    public void eat() {
        System.out.println("Eatting Food: " + carrot);
    }
}

eat 메서드를 다음과 같이 선언하고 프로그램을 실행해보자

 

실행 단축키는 ctrl + shift + f10을 누르면 된다.

오류없이 실행되는 것을 볼 수 있다.

 

Carrot에 여러 메서드를 정의해보자.

 

당근을 먹는 행위 외에 무엇을 할 수 있을까? 버릴 수도 있고, 자를 수도 있다. 먹기 싫다면 던져버릴 수도?

ChatGPT로 생성한 이미지다.

당근의 4가지 행위를 메서드로 작성해보자

eat(), cut(), shoot(), dump()

package com.learnspringframework.learnspringframework.food;

public class Carrot {
    public void eat(){
        System.out.println("Eatting...");
    }
    public void cut(){
        System.out.println("Cutting...");
    }

    public void shoot(){
        System.out.println("Shooting...");
    }

    public void dump(){
        System.out.println("dump...");
    }
}

그리고 Meal의 eat 메서드를 action 메서드로 수정해준다.

package com.learnspringframework.learnspringframework.food;

public class Meal {
    Carrot carrot;
    public Meal(Carrot carrot) {
        this.carrot = carrot;
    }

    public void action() {
        System.out.println("Eatting Food: " + carrot);
        carrot.eat();
        carrot.cut();
        carrot.shoot();
        carrot.dump();
    }
}

carrot의 메서드들을 실행하게 만들어주고 main에서 action메서드를 호출해주면

오류 없이 실행이 잘된다.

드디어 이번 포스팅의 메인주제가 등장했다.

우리가 작성한 코드에는 한가지 문제점이 있다.

 

만약에 우리가 먹고 싶은 음식이 당근이 아니라면 어떻게 될까?

 

우리가 작성한 클래스에 따르면 우리는 식사하는 동안 당근밖에 못먹는 끔찍한 일이 벌어진다.

당근 혁명이다!

농담이고, 당근이 아닌 다른 음식을 먹으려고 한다고 쳐보자.

package com.learnspringframework.learnspringframework;

import com.learnspringframework.learnspringframework.food.Carrot;
import com.learnspringframework.learnspringframework.food.Meal;
import com.learnspringframework.learnspringframework.food.PorkChop;

public class App01MealBasicJava {
    public static void main(String[] args) {
        // var carrot = new Carrot();
        var porkchop = new PorkChop();
        var meal = new Meal(porkchop);
        meal.action();
    }
}
package com.learnspringframework.learnspringframework.food;

public class PorkChop {
    public void eat(){
        System.out.println("Eatting...PorkChop");
    }
    public void cut(){
        System.out.println("Cutting...PorkChop");
    }

    public void shoot(){
        System.out.println("Shooting...PorkChop");
    }

    public void dump(){
        System.out.println("dump...PorkChop");
    }
}

PorkChop 클래스는 기존의 Carrot 클래스와 동일한 구조를 가졌지만 출력이 조금 다르다.

 

이제 이 프로그램이 동작하는지 확인을 해보면

컴파일 오류가 나게 된다.

Meal 클래스를 확인해보면 바로 이유를 알 수 있다.

 

클래스 자체가 Carrot으로 연결되어 있다. 즉 Carrot밖에 받지 못하는 상태라는 것.

 

이러한 현상을 우리는 강한 결합(Tight Coupling)이라고 부르게 된다.

 

만약 우리가 PorkChop 클래스를 사용하고자 한다면 코드를 다음과 같이 변경해주어야 한다.

매번 이렇게...?

하지만 코드가 수정된 지금도 PorkChop으로 결합이 바뀌었을 뿐 문제는 여전히 남아있다.

 

새로운 음식이 추가될 때 마다 새로운 Meal 클래스를 만들 수도 없는 노릇이고, 매번 수정해주기도 쉽지않다.

 

이 때 느슨한 결합(Loose Coupling)을 사용하면 유동적으로 변화하는 Meal클래스를 만들 수 있다.

인터페이스(Interface)

흔히 뉴비 절단기라고 부르는 추상 객체 대목이다.

 

추상클래스니 뭐니 복잡한 설명을 치우고 인터페이스 자체에 대해서 설명하자면

 

어떠한 클래스가 A가 있고 A가 인터페이스 B를 상속한다면

 

B의 구조를 따라 A가 작성되어야 한다.

 

즉 B에 eat(), cut(), shoot(), dump() 4개의 메서드가 존재한다면, A에도 똑같이 4개의 메서드가 작성되어야 한다.

 

어떠한 클래스의 구조를 정의한 설계도라고 표현할 수 있다.

 

인터페이스를 도입하면 현재 문제를 해결할 수 있다.

 

Food 인터페이스를 새롭게 추가해준다.

그리고 4개의 메서드를 작성해주자.

package com.learnspringframework.learnspringframework;

public interface Food {
    void eat();
    void cut();
    void shoot();
    void dump();
}

그리고 Meal 클래스와 PorkChop 클래스를 다음과 같이 수정해주자.

package com.learnspringframework.learnspringframework.food;

import com.learnspringframework.learnspringframework.Food;

public class Meal {
    private Food food;
    public Meal(Food food) {
        this.food = food;
    }

    public void action() {
        System.out.println("Eatting Food: " + food);
        food.eat();
        food.cut();
        food.shoot();
        food.dump();
    }
}

인터페이스를 사용하는 방법은 클래스 선언문 뒤에 implements [인터페이스 명]을 붙여주면 된다.

 

이제 PorkChop 클래스는 Food 인터페이스의 일부가 되었기 때문에 Food가 PorkChop을 받아도 문제가 없다.

 

main문은 이렇게 수정해주자.

package com.learnspringframework.learnspringframework;

import com.learnspringframework.learnspringframework.food.Carrot;
import com.learnspringframework.learnspringframework.food.Meal;
import com.learnspringframework.learnspringframework.food.PorkChop;

public class App01MealBasicJava {
    public static void main(String[] args) {
    // 포크찹과 당근의 변수명을 변경했다.
//        var food = new Carrot();
        var food = new PorkChop();
        var meal = new Meal(food);
        meal.action();
    }
}

PorkChop을 아무 문제 없이 받아서 실행했다.

 

이번엔 Carrot을 테스트 해보자

 

PorkChop을 주석처리하고 Carrot을 주석해제 해보자.

 

컴파일 에러가 난다.

 

왜냐하면 Carrot은 아직 Food의 일부가 아니기 때문이다.

 

마찬가지로 implements Food를 Carrot 선언문에 추가해주면 된다.

 

그러고 나서 Ctrl + Shift + F10으로 실행해주면....

Carrot도 문제없이 받아서 실행한다.

 

이제 어떤 클래스든 Food의 구조를 받아서 선언해준 뒤 Meal에 넘겨주면

 

문제 없이 실행된다.

 

우리는 이러한 구조를 느슨한 결합이라고 부른다.

 

마무리

이번 포스팅에선 강한 결합과 느슨한 결합이 무엇인지 공부했다.

 

인터페이스란 구조도 배웠고, 인텔리제이의 사용법도 대충 배웠다.

 

한번 혼자서 새로운 클래스 Chicken을 선언해서 한번 인자로 넘겨주면서 연습해보자.