...
소프트웨어의 모듈 독립성
자고로 프로그래밍이란 문제를 정의하고 하나씩 해결해나가는 과정을 말한다.
만약 문제가 복잡하고 큰 문제라면, 일반적으로 문제를 작은 부분으로 쪼개어서 하나씩 풀어나가게 될 것인데, 이 때 문제를 작은 부분으로 쪼개나가는 것을 모듈화라고 한다.
모듈은 소프트웨어를 각 기능별로 나누어진 소스 단위를 말한다. 독립적으로 컴파일 가능한 프로그램 혹은 하나의 함수나 클래스도 모듈이 된다.
보통 좋은 소프트웨어 일수록 모듈의 독립성이 높다고 한다.
좋은 모듈화는 목적에 맞는 기능만으로 모듈을 나누게 되는데, 각각의 모듈은 주어진 기능만을 독립적으로 수행하기 때문에 재사용성이 높고 코드의 이해/수정이 용이하기 때문이다.
예를들어 해당 모듈을 수정하더라도 다른 모듈에 끼치는 영향이 적게 되며, 오류가 발생하더라도 기능 단위로 잘 나뉘어 져 있기 때문에 손쉽게 문제를 발견해 해결할 수 있기도 한다.
이러한 모듈의 독립성은 모듈의 결합도(Coupling) 와 응집도(Cohesion)의 기준 단계 측정한다
간단하게 이들을 소개하자면, 결합도는 모듈과 모듈 간의 의존 정도를 의미하고, 응집도는 한 모듈 내의 구성요소들 간의 연관 정도를 의미한다.
그리고 결합도와 응집도의 강도 세기에 따라 여러 단계로 나뉘게 되는데 응집도는 강할수록, 결합도는 느슨할 수록 독립성이 높은 모듈로 평가 된다.
이제부터 이들에 대해 자세히 알아보자.
결합도(Coupling)
결합도는 모듈(클래스 파일)간의 상호 의존 정도 또는 연관된 관계의 끈끈함 정도를 의미한다고 보면 된다. (결합도를 의존도라고 부르기도 한다)
예를 들면 결합도가 높은 클래스는 다른 클래스와 연관 관계가 끈끈하여, 만일 하나의 클래스의 구조를 변경하게 된다면 그에 연관된 클래스들도 싹 변경해야 할수도 있고, 객체 사용 코드도 변경해야 할 수도 있어서, 유지보수 측면에서 매우 마이너스적인 요소로 작용된다.
실생활로 비유하자면 완제품 자동차 하나에는 여러개의 모듈들 핸들, 바퀴, 엔진, 배터리 등이 들어있을 것이다. 그리고 이렇게 하나의 프로그램(자동차) 안에서 각 모듈들이 서로 관련되어 의존하고 있는 정도가 결합도 이다.
핸들과 바퀴 모듈간의 관계를 예로 들자면, 이 둘은 각각의 동작이 상호작용을 통해 자동차가 굴러가기 때문에 어느정도의 결합도가 생길 수 밖에 없다. 하지만 그렇다고 해서 바퀴를 교체하는데 핸들까지 교체해야 된다면 이건 상식적으로 자동차 설계 부터가 잘못 되었다고 말할 수 있다.
객체지향의 관점에서 결합도는 클래스 또는 메서드가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는지를 나타낸다.
즉, 어떤 클래스가 다른 클래스에 대해 너무 자세히 알고 있다면 두 모듈은 높은 결합도를 가지게 되고, 반대로 어떤 클래스가 다른 클래스에 대해 꼭 필요한 지식만 가지고고 있다면 두 모듈은 낮은 결합도를 가진다고 말할 수 있는 것이다.
만약 클래스들 간에 연관이 있을때 인터페이스로 제대로 분리되어 있지 않고 불필요하게 많은 정보를 알고 있다면 이는 결합도가 높게 측정되게 된다.
따라서 좋은 소프트웨어는 낮은 결합도(low coupling)를 가지고 있다고 말할 수 있다.
[결합도가 낮은 클래스의 특징]
- Open Closed Principle의 원칙을 잘 지킨 클래스 (확장에는 열려있고 변경에는 닫혀 있는 클래스)
- 다형성을 잘 지킨 클래스
결합도 단계 종류
위에서 언급한 바와 같이 결합도는 강도와 세기에 따라 여러 단계로 종류별로 나뉘어 진다.
다음과 같이 결합도는 자료, 스탬프, 제어, 외부, 공통, 내용 결합도의 척도로 나타낼 수 있다.
세기 | 종류 | 내용 |
약함 ↓ 강함 |
자료 결합도 (Data Coupling) |
모듈간의 인터페이스로 전달되는 파라미터(데이터)를 통해서만 상호 작용이 일어나는 경우 결합도가 제일 낮고 제일 좋은 형태이다. |
스탬프 결합도 (Stamp Coupling) |
모듈간의 인터페이스로 배열이나 객체, 자료 구조 등이 전달되는 경우 | |
제어 결합도 (Control Coupling) |
어떤 모듈이 다른 모듈 내부의 논리적인 흐름을 제어하는 제어 요소를 전달하는 경우 | |
외부 결합도 (External Coupling) |
어떤 모듈이 외부에 있는 다른 모듈의 데이터를 참조하는 경우 (데이터, 통신 프로토콜 등) | |
공통 결합도 (Common Coupling) |
여러 개의 모듈이 하나의 공통 데이터 영역(전역 변수 참조 및 갱신)을 사용하는 경우 | |
내용 결합도 (Content Coupling) |
어떤 모듈 내부에 있는 변수나 기능을 다른 모듈에서 사용하는 경우 결합도가 제일 높고 제일 좋지 않은 형태이다. |
자료 결합도 (Data Coupling)
- 가장 결합도가 낮고, 가장 좋은 형태
- 모듈끼리 단순히 데이터를 주고 받는 경우 (기능 수행에 있어서 로직을 제어하거나 하지 않는 순수한 자료형 요소의 데이터를 주고 받는 것)
- 한 모듈을 변경하더라도 다른 모듈에는 영향을 끼치지 않는 결합 형태
class Bill {
// 주차 요금 청구서 모듈 (사용시간과 할인률을 인자로 받고 계산하여 주차요금 메소드를 호출)
public static int getBillFee(int time, int discount) {
double discountPercentage = discount / 100;
// 메서드안에서 또 다른 메서드를 호출해 의존도가 있긴 하지만, 메서드에 단순 파라미터 데이터를 보내는 형태
return Fee.calculateFee(time) * discountPercentage;
}
}
class Fee {
// 주차 요금 계산 모듈
public static int calculateFee(int time) {
int defaultMoney = 1000;
return defaultMoney * time;
}
}
public class Main {
public static void main(String[] args) {
Bill.getBillFee(2, 80); // 2시간 이용, 80% 할인
}
}
스탬프 결합도 (Stamp Coupling)
- 두 모듈이 인터페이스로 배열이나 오브젝트와 같은 동일한 자료 구조를 참조하는 형태의 결합도
- 만일 모듈에 쓰일 자료구조 형태가 변경되면 그것을 참조하는 모든 모듈에 영향을 주게 됨
// 이용기록 자료구조 형태
class Record {
public int carNum;
public int useTime
private int fee
Record(int carNum, int useTime) {
this.carNum = carNum;
this.useTime = useTime;
}
public void setFee(int fee) {
this.fee += fee;
}
public int getFee(int fee) {
return this.fee;
}
}
class BillFee {
// 주차장 이용 청구서 작성 모듈
public static int getBillFee(String name, int hour) {
Record record = new Record(name, hour); // 자료구조 생성
Bill.calculateFee(record); // 자료구조를 메서드에 넘김
Fee.calculateFeeAnother(record); // 자료구조를 메서드에 넘김
return record.getFee();
}
}
class Bill {
// 주차비용 계산 모듈
public static void calculateFee(Record record) {
int defaultMoney = 1000;
record.setFee(defaultMoney * record.useTime);
}
}
class Fee {
// 기타비용 계산 모듈
public static int calculateFeeAnother(Record record) {
int defaultMoney = 5000;
record.setFee(defaultMoney * record.useTime);
}
}
위의 스탬프 결합도의 코드 예제를 살펴보면, 앞서 살펴본 자료 결합도(Data Coupling)는 단순 데이터 타입을 넘겼지만 스탬프 결합도는 오브젝트 형태의 자료 구조를 전달한다는 점이 다르다.
이런식으로 자료 구조를 넘겨 의존시키게 되면, 만일 클래스의 필드명이나 구성이 변경되는 경우 이를 참조하는 모듈에도 변경이 필요할 수 있다. (클래스 필드명을 useTime을 useHour 이라고 바꿀 경우 메서드 2개를 통짜로 직접 수정해야 한다)
제어 결합도 (Control Coupling)
- 어떤 모듈이 다른 모듈 내부의 논리적인 흐름을 제어하는 제어 요소를 전달하는 경우
- 상위 모듈이 하위 모듈의 상세한 처리 절차를 알고 있어 이를 통제하는 경우
- 제어 결합은 정보은닉을 위배하는 결합으로, 한 모듈이 다른 모듈 내부에 관여하여 관계가 복잡해짐
// 회원 정보 클래스
class User { ... }
class Bill {
// 주차 요금 청구서 모듈
public static int getBillFee(int time) {
double discountPercentage = discount / 100;
User user1 = new User();
// 파라미터로 전달되는 값에 따라서 모듈 내부 로직의 처리가 달라지는 결합 형태
return Fee.calculateFee(time, user.isJoin) * discountPercentage;
}
}
class Fee {
// 주차 요금 계산 모듈
public static int calculateFee(int time, boolean isJoin) {
int defaultMoney = 1000;
// 회원 여부에 따라 주차요금을 계산하는 로직이 달라지게 된다
if(isJoin) {
return defaultMoney * time - 400; // 회원 이면 400원 할인
} else {
// ...
}
}
}
외부 결합도 (External Coupling)
- 모듈이 외부에 있는 다른 모듈의 데이터를 참조할 때의 결합도
- 외부의 데이터, 통신 프로토콜 등을 공유할때 발생 (참조할 데이터가 외부 모듈에 위치할때)
- 어떤 외부 모듈에서 반환한 값을 다른 모듈에서 참조하는 경우
- 참조되는 데이터의 범위를 각 모듈에서 제한할 수 있다
class AddFee {
private int plusFee;
private int plusTime;
AddFee(int p1, int p2) {
// ...
}
// Getter / Setter ...
}
// 외부 모듈을 가져오기
import AddFee;
class Bill {
// 주차 요금 청구서 모듈
public int getBillFee(int time, int discount, Addfee f) {
double discountPercentage = discount / 100;
return calculateFee(time, f) * discountPercentage + f.getPlusFee(); // 외부 모듈 f에 있는 데이터 참조
}
}
class Fee {
// 주차 요금 계산 모듈
public int calculateFee(int time, Addfee f) {
int defaultMoney = 1000;
return defaultMoney * (time + f.getPlusTime); // 외부 모듈 f에 있는 데이터 참조
}
}
public class Main {
public static void main(String[] args) {
int a = 1000;
int b = 10;
// 외부 모듈의 데이터를 결정하는 파라미터 a와 b를 넣어주고 외부 데이터를 반환
Addfee addfee = new Addfee(a, b);
getBillFee(2, 80, addfee);
}
}
// main 메소드
공통 결합도 (Common Coupling)
- 여러 개의 모듈이 하나의 공통 데이터 영역을 사용하는 결합도
- 대표적으로 전역 변수(global variable)를 예로 들 수 있음 (전역 변수의 변경이 여러 모듈에 영향을 줌)
- 공통 데이터 영역의 내용을 조금만 변경하더라도 이를 사용하는 모든 모듈에 영향을 미침
- 위의 외부 결합도와 유사하게 볼수 있으나 공통 데이터가 외부냐 내부냐에 따라 차이가 있다.
- 공통 결합도가 외부 결합도 보다 결합도가 높은 이유는, 전역 변수의 값에 따라 결국은 외부의 모듈 반환값 까지 결정될 수 있는 가능성이 있기 때문에 외부 결합도 보다 더 결합도가 높도록 평가된다고 보면 된다.
class Discount {
public static int discount = 50; // 공통 전역 데이터
public static int discountDefault = 500; // 공통 전역 데이터
}
class Bill {
// 주차 요금 청구서 모듈
public int getBillFee(int time) {
double discountPercentage = Discount.discount / 100; // 공통 데이터 사용
return calculateFee(time) * discountPercentage;
}
}
class Fee {
// 주차 요금 계산 모듈
public int calculateFee(int time) {
int defaultMoney = 1000 - Discount.discountDefault; // 공통 데이터 사용
return defaultMoney * time;
}
}
내용 결합도 (Content Coupling)
- 가장 높은 결합도를 갖으며, 가장 좋지 않은 결합 형태
- 어떤 모듈이 사용하려면 다른 모듈의 내부 기능과 데이터를 직접 참조해 그대로 가져와 사용하거나 수정하는 경우
- 이렇게 되면 A모듈, B모듈 모두 코드를 알고 있어야 하며 A모듈이 변경되면 B모듈도 영향을 미쳐 변경해야 함
응집도(Cohesion)
응집도는 하나의 클래스가 기능에 집중하기 위한 모든 정보와 역할을 갖고 있어야 한다는 의미이다.
정확히 응집도는 한 모듈 내의 구성 요소 간의 밀접한 정도를 의미하는데, 한 모듈이 하나의 기능(책임)을 갖고있는 것은 응집도가 높은 것이고, 한 모듈이 여러 기능을 갖고 있는 것은 응집도가 낮은 것이다.
응집도가 높은 모듈은 하나의 모듈 안에 함수나 데이터와 같은 구성 요소들이 하나의 기능을 구현하기 위해 필요한 것들만 배치되어 있고 긴밀하게 협력한다. 반대로 응집도가 낮은 모듈은 모듈 내부에 서로 관련 없는 함수나 데이터들이 존재하거나 관련성이 적은 여러 기능들이 서로 다른 목적을 추구하며 산재해 있다.
예를들어 쇼핑몰 프로젝트에서 주문 처리를 담당하는 클래스에서 회원의 정보를 업데이트하는 메서드가 있다면, 이것은 응집도가 낮은 것이다. 회원 정보 업데이트는 회원만 담당하는 클래스에서 따로 분리하여 처리하는것이 옳기 때문이다.
이처럼 응집도가 높은 모듈은 기능을 수정할 때 관련 내용이 하나의 모듈에 모여있으므로 코드를 이해하기도 쉽고, 수정한 후 관련 없는 다른 모듈에겐 영향을 주지 않아 코드의 유지보수에 유리하다.
객체지향의 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 있는 책임들을 할당했는지를 나타낸다.
나의 코드가 응집도가 높은지 검사해보는 방법은 메소드들에 대하여 인스턴스 변수를 사용하는 비율이 높은지 보는 것이다. 어떤 클래스의 응집도가 높다면 메소드와 변수가 서로 의존하고 있을 것이고, 응집도가 낮다면 상태와 기능의 논리적 연결이 약할 것이고 이는 클래스를 더욱 분리할 수 있음을 암시하기도 한다.
즉, 응집도가 높을 수록 독립성이 높은 모듈이며 좋은 소프트웨어는 높은 응집도(high cohesion)을 유지해야 한다.
[응집도가 높은 클래스의 특징]
- 단일 책임을 가진 클래스 (Single Responsibility Principle)
- 다른 클래스와 잘 협력하는 클래스
응집도 단계 종류
응집도의 강도 단계는 기능적, 순차적, 교환적, 절차적, 시간적, 논리적, 우연적 응집도의 척도로 분리 되며, 이에 따라 모듈의 품질을 측정할 수 있다. 기능적 응집도 쪽으로 갈 수록 좋은 품질이라고 할 수 있으며 우연적 응집도로 갈수록 나쁜 품질이 된다.
세기 | 종류 | 내용 |
강함 ↓ 약함 |
기능적 응집도 (Functional Cohesion) |
모듈 내부의 모든 기능이 단일 목적을 위해 수행되는 경우 응집도가 제일 높고 제일 좋은 형태이다. |
순차적 응집도 (Sequential Cohesion) |
모듈 내에서 한 활동으로부터 나온 출력 값을 다른 활동이 사용할 경우 | |
교환적 응집도 (Communication Cohesion) |
동일한 입력과 출력을 사용해 다른 기능을 수행하는 활동들이 모여있을 경우 | |
절차적 응집도 (Procedural Cohesion) |
모듈이 다수 관련 기능을 가질 때 모듈 안의 구성요소들이 그 기능을 순차적으로 수행할 경우 | |
시간적 응집도 (Temporal Cohesion) |
연관된 기능이라기 보단 특정 시간에 처리되어야 하는 활동들을 한 모듈에서 처리할 경우 | |
논리적 응집도 (Logical Cohesion) |
유사한 성격을 갖거나 특정 형태로 분류되는 처리 요소들이 한 모듈에서 처리되는 경우 | |
우연적 응집도 (Coincidental Cohesion) |
모듈 내부의 각 구성요소들이 연관이 없을 경우 응집도가 제일 낮고 제일 좋지 않은 형태이다. |
기능적 응집도 (Functional Cohesion)
- 가장 응집도가 높은 형태로 가장 좋은 형태이다.
- 모듈 내부의 모든 기능이 단일 목적을 위해 수행되는 경우
- 대입 되는 변수가 공통적으로 사용되는 경우
- 대표적인 예로 수학 연산에 관련된 모듈들을 모은 Math 클래스를 들 수 있다.
public class Stack {
private int topOfStack = 0;
private List<Integer> elements = new LinkedList<Integer>();
public int size() {
return topOfStack;
}
public void push(int element) {
topOfStack++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty {
if (topOfStack == 0) {
throw new PoppedWhenEmpty();
}
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}
위의 코드는 Stack 자료구조을 구현한 클래스이다.
보통 메서드가 인스턴스 변수를 많이 사용하면 할 수록 메서드와 클래스는 응집도가 높아지는데, 위의 코드에서는 각 메서드 마다 인스턴스 변수를 이용해 자료를 업데이트 해줌으로써 응집도를 높였다.
순차적 응집도 (Sequential Cohesion)
- 모듈 내에서 한 활동으로 부터 나온 출력값이 다음 활동의 입력 데이터로 사용할 경우
- 어떤 모듈이 특정 파일을 읽고 처리하는 기능을 하는 등과 같다
class Sequential {
void processGrade(Grade grade) {
String numberGrade = grade.getGrade(); // 어느 모듈의 출력값이
String letterGrade = grade.computeLetter(numberGrade); // 바로 다음 입력값으로 이용
grade.displayLetter(letterGrade); // 다시 전의 반환값이 바로 다음 입력값으로 이용
// 앞의 값이 있어야 뒤의 모듈을 실행할 수 있기 때문에 처리 순서가 있음
// 메소드의 리턴 된 결과가 다음 메소드의 입력 파라미터로 쓰이는 경우
}
}
교환적 응집도 (Communication Cohesion)
- 통신적 응집도 라고도 불림
- 동일한 입력과 출력을 사용하여 다른 기능을 수행하는 활동들이 모여있는 경우
- 메소드 호출에 공통된 파라미터가 입력되는 경우
- 순차적 응집도와 차이점은 대신에 처리 순서가 중요치 않다는 것이다.
class Communicational {
void Compute_MatrixMatrix(Matrix marix) {
int[][] aMatrix = marix.setGraph({ // 어느 모듈의 출력값이
{0, 0, 0},
{0, 0, 0}
});
transform_matrix = marix.trans(aMatrix); // 바로 다음 입력값으로 이용
inverse_matrix = marix.inverse(aMatrix); // 바로 다음 입력값으로 이용
// 다만 앞의 값으로 서로 다른 기능의 모듈을 수행하는 것이기 때문에 모듈 처리 순서는 상관 없음
// 메소드 호출에 공통된 파라미터가 입력
}
}
절차적 응집도 (Procedural Cohesion)
- 모듈이 다수 관련 기능을 가질 때 모듈 안의 구성요소가 그 기능을 순차적으로 수행할 경우
- 하나의 클래스에 있는 메소드들을 여러 개 호출하는 경우
class Procedural extends Letter {
void sendLetter() {
// 하나의 클래스에 있는 메소드들을 여러 개 호출하는 경우
this.writerBody();
this.writerSalutation();
this.send();
}
}
시간적 응집도 (Temporal Cohesion)
- 일시적 응집고 라고도 불림
- 각 기능 요소들이 순서에 상관없이 특정 시점에 반드시 수행되는 경우
- 연관된 기능이라기 보단 특정 시간에 처리되어야 하는 활동들을 한 모듈에서 처리할 경우
- 메소드 호출이 일어나지 않고 변수의 초기화만 실행되거나, Exception 에러 로그를 보내거나 등
class Temporal {
int no_student;
int no_department;
String university_name;
void Init_Variables() {
// 특정 시간에 처리되어야 하는 활동들을 한 모듈에서 처리 (변수 초기화)
no_student = 0;
no_department = 0;
university_name = "Hongik University";
}
}
논리적 응집도 (Logical Cohesion)
- 유사한 성격을 갖거나 특정 형태로 분류되는 처리 요소들이 한 모듈에서 처리되는 경우
- 논리적으로 비슷한 기능을 수행하지만 서로의 관계는 밀접하지 않은 형태
- switch문이 쓰여 case에 따라 비슷하지만 다른 작업을 수행하는 경우
class Logical {
public void solve_equation(int no_equ, long x) {
// switch문으로 논리적으로 분리된 기능을 수행하는 것처럼 보이지만, 서로 다른 모듈을 실행하는 관계가 없는 상태
switch (no_equ) {
case 1:
// 모듈 실행
break;
case 2:
// 또다른 모듈 실행
break;
case 3:
// 또다른 또다른 모듈 실행
break;
}
}
}
class SetProp {
// height와 width 인스턴스 값을 동시에 설정하는 Setter 모듈
public void setValue(String name, int value) {
// 기능 동작 자체로는 문제는 없지만, 하나의 메서드에서 전부 처리하는것보다, 각각의 메서드에 하나의 인스턴스를 처리하도록 짜는게 좋다.
if ("height".equals(name)) {
this.height = value;
}
if ("width".equals(name)) {
this.width = value;
}
}
}
우연적 응집도 (Coincidental Cohesion)
- 가장 좋지 않은 응집도
- 모듈 내부의 각 구성 요소들이 서로 관련없는 요소로만 구성된 경우
class Coincidental extends RecordProcess {
int average, totalScore;
boolean done;
Letter l;
void doStuff() {
// 무지성으로 서로 관련 없는 코드를 한 모듈안에 모아 실행
readRecords();
average = totalScore / 10;
l.printNames();
done = true;
return;
}
}
높은 응집도 & 낮은 결합도
결합도가 높은 클래스의 문제점은 클래스의 규모가 커지기 때문에 이해 하기 쉽지 않으며, 변화에 따른 다른 요소들의 변경을 예측하기 쉽지 않다.
응집도 낮은 클래스의 문제점은 코드를 이해하기가 힘들고, 재사용이 힘들다. 또한 유지보수가 매우 쉽지않으며 클래스 변화에 민감하다
따라서 높은 응집도와 낮은 결합도가 이상적인 소프트웨어 모듈이게 된다.
하지만 그렇다고 응집도는 높을수록 좋고 결합도는 낮을 수록 좋다는 "high cohesion loose coupling" 원칙이 항상 유효한 것은 아니다.
결국 프로그램의 특성이나 상황에 따라 유연하게 적용해야 한다.
하지만 시스템 유지보수 측면에 있어서 모듈의 응집도는 높을수록, 결합도는 낮을 수록 유리한 것은 분명하다.
특히나 남이 개발해놓은 엉망진창인 코드를 유지보수 해야할 때 비로소 체감하게 되는 것이 응집도와 결합도 이다.
한번쯤은 들어본 객체지향의 여러가지 유명한 디자인 패턴들이 바로 이 응집도를 높히고 결합도를 낮추기 위한 원칙으로 설정되고 구성된 요소이다. 그래서 디자인 패턴의 원칙을 잘 지키면 하나하나 단계를 따질 필요없이 자연스럽게 "high cohesion loose coupling" 따르게 되니 다들 입을 모아 중요하다고 하는 것이다.
# 참고자료
https://madplay.github.io/post/coupling-and-cohesion-in-software-engineering
https://rutgo-letsgo.tistory.com/227
https://feature-sliced.design/docs/concepts/low-coupling
https://medium.com/@jang.wangsu/%EC%84%A4%EA%B3%84-%EC%9A%A9%EC%96%B4-%EC%9D%91%EC%A7%91%EB%8F%84%EC%99%80-%EA%B2%B0%ED%95%A9%EB%8F%84-b5e2b7b210ff
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.