디자인 패턴을 배워보자 3일차 - 디자인 패턴 원칙(SOLID)

0

Design pattern

목록 보기
3/3

디자인 패턴 basic

다지인 패턴과 원칙(Design patterns and principles)

성공적인 소프트웨어 개발에 관해 이야기할 때, application이 설계된 바 어떻게 동작하는 지
논하는 것 뿐만 아니라, 얼마나 어플리케이션을 테스팅하고 관리하는 데 노력이 들어갈 지에 대해서도 고민해야한다.

소프트웨어 개발은 계속해서 요구사항에 맞춰서 변화하고 개발되어야 한다. 때문에 계속해서 변화하는 요구에 있어 소프트웨어는
유연해야하고 쉽게 변화할 수 있어야 하는 것이다.

이러한 문제를 해결하는 것이 바로 OOP의 디자인 패턴과 원칙이다. OOP의 디자인 원칙은 SOLID라고 불린다. 이 원칙은 소프트웨어를 설계하고
개발할 때 적용하는 일련의 룰으로서 프로그램을 개발하기 쉽고, 유지 보수하기 쉽게 만들어준다.

SOLID는 약자로서 5가지 원칙을 의미한다. 1.Single responsibility , 2. open/closed principle,
3. Liskov substitution principle, 4. Interface segregation principle, 5. Dependency inversion principle

또한, 디자인 원칙들 안에는 디자인 패턴이 존재한다. 디자인 패턴은 주로 발생하는 문제들에 적용되는 재사용 가능한 일반적 솔루션들을 말한다.

1. Single Responsibility Principle(단일 책임 원칙)

단일 책임 원칙은 '소프트웨어 모듈은 반드시 변화를 일으키는 이유(책임)가 하나만 있어야 한다'는 것이다. 즉, 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 한다는 것이다.
따라서 클래스가 제공하는 모든 기능은 이 책임과 주의 깊게 부합해야 한다.

단일 책임 원칙은 캡슐화가 가장 잘 발현되었다고 볼 수 있다. 변화를 일으킨다는 것은, 곧 기능을 한다는 것이고 이는 코드를 변경해야하는 필요성을 유발한다.
클래스가 변화를 일으킴 -> 클래스가 특정 기능을 담당하고 있다는 것이다. -> 해당 기능의 변경이 생기면 클래스를 변경해야 한다 는 것이다. 따라서, 클래스가 두 가지 다른 속성의 변화를 일으킨다는 것은
두 가지 기능을 한다는 의미이고, 이는 두 가지 책임을 지녔다는 의미이다. 이는 클래스의 코드가 변화되어야 하는 이유가 두 가지라는 의미이다.

이러한 변화가 하나의 모듈(클래스)에서 다른 함수로 분리된다 하더라도, 일련의 변화들이 다른 기능에 대해 변화를 일으킬 수 있다. 가령 다음과 같은 예제를 확인해보자,

다음의 예시에서 Car 클래스가 기본적으로 Car에 대한 정보 기능과 데이터 베이스 접근 기능 두 가지를 갖고 있다고 해보자,
하나는 Car의 기본적인 정보 로직으로 name, model, year 속성과 calculatePrice() 메서드로 구성된다.
다른 하나는 Car클래스가 데이터 베이스에 접근하여 정보를 변경하는 create, read, update, delete를 가지고 있다.

이는 Car 클래스가 기본적인 자동차 정보를 변화할 수 있고, 디비의 상태를 변화할 수 있다는 것이고, 곧 이는 자동차 정보 기능에 대한 책임, 자동차 정보에 관한 디비 접근 기능에 대한 책임으로 이어진다.
지금은 별 문제없어보이지만, 하나의 클래스에서 두 가지 이상의 책임을 갖고 있으면 이들의 coupling이 높아져버려 원치 않은 변경을 해야할 수 있다.

가령, 지금은 Car 클래스에 name, model ,year과 같은 정보가 맴버 변수로 주어져, 데이터 베이스에 접근할 때 넣어준다. 이는 만약 넣으려는 정보가 많아지면 많아질 수록, 또는 적어지면 적어질 수록
Car 클래스의 기본적인 맴버 변수에도 변경이 필요하고, 데이터 베이스 접근 메서드 기능에도 변경이 필요하다는 것이다. 설령 추가된 맴버변수가 Car클래스에서 자동차의 정보 표현에 필요 없는 기능이라도 데이터베이스에 넣어지려면
억지로라도 들고있어야 한다는 것이다.

이러한 문제를 해결하기위해 우리는 한 클래스에 두 가지 책임 이상을 두지 않도록 한다. 그래서 두 책임을 하나씩 나누는 것이다. 위의 예제에서는 기본적인 자동차 정보 , 기능
데이터 베이스 접근 기능을 나눈다.

CarInfo는 자동차의 기본적인 정보를 제공하는 책임을 가진다. CarDAO는 데이터 베이스에 자동차 정보를 불러오고, 변화를 일으키는 책임을 가진다. 이에 따라, CarDAOcreaate, read, update, deleteCarInfo 클래스 정보를 입력받는다.
이제 데이터 베이스에 들어가는 정보가 추가되어도 CarInfo에 영향을 주지 않는다.

가령, 데이터 베이스에 구매한 날짜를 추가한다고 해서, CarInfo구매한 날짜를 굳이 넣어줄 필요는 없다. 왜냐하면 CarDAO 메서드에 매개변수로 구매한 날짜를 넣어줘도 되는 것이고, 다른 클래스나 함수로부터 정보를 읽어와도 된다. 즉, CarInfo에서 구매한 날짜가 그다지
필요하지도 않은데 가지고 있을 필요가 없다는 것이다.

또한, 이렇게 분리함으로서 얻을 수 있는 장점은 테스트하기가 더 좋아진다는 것이다. 이제 CarDAO클래스의 매개변수로 CarInfo만 주면될 뿐이다. 이전처럼 Car의 맴버 변수를 모두 초기화해놓고 설정할 필요가 없다는 것이다.

이처럼, 단일 책임 원칙은 하나의 모듈(클래스)에 하나의 책임만을 부여하여 기능을 단순화하고, 결합도를 줄여 유지보수와 테스트를 더욱 용이하게하는 장점이 있다.

2. Open/Closed Principle(개방-폐쇄 원칙)

개방-폐쇄 원칙은 클래스 또는 모듈에 대해서 확장은 개방(open)하고, 변경, 수정은 폐쇄(close) 하라는 것이다.

기존의 있던 코드에 새로운 코드를 추가한다거나, 변경하는 것은 매우 위험한 작업이다. 복잡한 소프트웨어 시스템을 개발한다고 했을 때,
하나의 모듈에 대해서 test를 진행하고 다음 개발로 넘어간다고 하자. 이 때, 추후에 다시 이 모듈의 기능을 변경하거나, 추가하게된다면
이 모듈은 이전의 기능도 있어야하고, 추가, 변경한 기능도 반영되어야 한다. 그런데 이를 반영하는 것은 쉬운 일이 아니다. 변경으로서 기존의 기능들이
작동하지 않는 경우도 있고, 변경한 기능이 기존 기능과 conflict를 발생하는 경우도 있기 때문이다.

다음의 예시를 보면, CarDriverCar에 의존하고 있다. 따라서, Car의 코드가 변경되면 이 영향은 CarDriver에게도 미치게 되는 것이다.
이를 해결하기위해, 우리는 확장을 허용하도록 한다. 단, 수정, 변경에 있어서는 폐쇄한다.

확장을 허용한다는 것은 상속(특정 클래스 extends), 구현(인터페이스 implementation) 을 통해 기능을 추가한다는 의미이다.
즉, Car의 클래스 자체를 바꾸는 것이 아닌, 이를 상속하거나 구현하고 있는 다른 클래스를 만들어 CarDriver에게 넣어주는 것이다.
이렇게하면 CarDriverCar에 의존하더라도 Car의 직접적인 변경, 수정이 이루어진게 아니라, 확장, 구현이 이루어졌기 때문에 별 문제가 없다.

다음과 같이 바꾸면, 똑같이 CarDriverCar에 의존하더라도, 코드의 변경, 수정은 직접적으로 Car에 이뤄지지 않아 직접적인 영향을 미치지 않는다. 따라서,
CarDriverCar에게서 알고 있는 정보들을 이용해서 만든 로직이 변경되는 일이 없다. 이는 Car에 대한 기능이 변경되는 것이 아닌 확장, 구현이 이루어졌기 때문이다.

3. Liskov Substitution Principle (리스코프 치환 원칙)

Barbara Liskov는 이렇게 기술했다. Derived types must be completely substitutable for their base types 이를 Liskov Substitution Principle(LSP)라고 한다.

이게 무슨 말이냐하면, 자식 클래스가 부모 클래스인 것 마냥 완전히 같은 것처럼 다루어져야 한다는 것이다. 가령, Car라는 부모 클래스가 있고,
Hyundai, BMW가 있다면, 이들은 코드상에서 Car로 다루어질 수 있어야 한다느 것이다. 즉, 부모 클래스를 자식 클래스로 치환해도 문제가 없어야 한다는 것이다.

리스코프 치환원칙은 다형성에 관한 이론으로, 정확히는 OOL(Object Oriented Language)에 있는 subtyping polymorphism에 기반한 것이다.

리스코프 치환원칙은 모듈들과 클래스들을 설계할 때, 자식 클래스가 행동(behavior)의 관점에서 부모와 치환되는 것이 보장되어야 한다.
자식 클래스가 부모 클래스에 치환될 대, 코드는 부모 클래스를 가지고 있는 것처럼 보이지만, 실제로는 자식 클래스를 가지고 있어 그 행동을 사용할 수 있다.
이러한 관점에서 자식 클래스는 부모 클래스처럼 행동해야하고, 행동을 부숴버리면 안된다. 이러한 것을 Stroing behavioral subtyping이라고 한다.

그런데, 어차피 자식 클래스가 부모 클래스를 상속받으면 부모 클래스에 치환될 텐데 무슨 의미가 있나? 싶을 것이다. LSP를 위반한 다음의 예시를 보도록 하자.

다음은 사각형정사각형의 관계를 나타낸 것이다. 정사각형은 사각형이다.(suqare is rectangle)이 성립하기 때문에
상속관계가 성립된다.

여기까지는 별다른 문제가 없어 보인다. 그런데, 사각형height, width를 설정하는 부분에 문제가 생긴다.

height, width 맴버 변수의 setter인 setHeight, setWidth 메서드를 추가하였다. 그런데, 문제는 rectangle은 정상인 반면, square는 정사각형이기 때문에
width, height가 동일해야한다. 따라서, 한쪽이 바뀌면 또 한쪽이 바뀌고, 변경되어야 하는 문제가 생기고, 의미상으로도 맞지가 않다. 이것이 바로 LSP가 지켜지지 않은 경우라고 볼 수 있다.
Rectangle 클래스 사용자는 setHeight, setWidth로 따로따로 높이, 넓이가 설정되기를 기대했을 것인데, 그렇지 않은 경우가 있어 코드 상에서 엄청난 오류를 불러낼 가능성이 있다.
즉, 의미상으로 이들은 is-a관계이므로 상속이 맞지만, 현실적으로 봤을 때 그리고 코드적으로 어색하거나 잘못된 설계부분이 들어난다는 것이다.

이를 해결하는 방법으로 클래스를 세분화시키는 방법이 있다.

다음처럼, Shape 클래스를 하나두고, Squre, Rectangle로 나눌 수 있다. 이렇게 리스코프 치환 원칙을 지켜주면 된다.

또 다른 예시로는 Bird와 Duck, Ostrich가 있다. 즉, 새와 오리, 타조라는 것인데, 간단히보면 다음과 같다.

'오리는 새이다' , '타조는 새이다' 따라서 다음과 같은 코드가 가능하다.

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}
public class Ostrich extends Bird{}

상속관계로 보면 아무런 문제가 없다. 그러나, 잘보면 Brid 클래스에는 fly가 있는데, Ostrich(타조)는 날 수 없다.
때문에 리스코프치환원칙을 어긴 것이다. fly메서드에 아무런 기능을 넣을 수 없기 때문이다. 그래서 의미없는 기능을 넣는다고해서 해결될 문제가 아니다.
만약, Bird클래스를 가져다가, fly 메서드를 호출해서 날기를 기대했는데, 날지를 못한다면 이는 코드상에서 엄청난 오류를 만들어낼 것이다.

그래서 다음과 같이 클래스를 세분화하여 설계할 수 있다.

public class Bird{}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}

FlyingBrids라는 클래스를 만들어 좀 더 세분화한 것이다. 이와 같은 경우에 FlyingBirds 사용자는 새가 날아가는 것이 문제가 없을 것이다.

4. Interface Segregation Principle (인터페이스 분리 원칙)

https://www.oodesign.com/interface-segregation-principle.html
clients should not be forced to depend upon interfaces(or methods) that they don't use

직역하면 '사용자들은 사용하지 않는 인터페이스(또는 기능)들에 의존을 강요되어서는 안된다.' 이다. 더 쉽게 말하자면, '사용자가 쓰는 기능만 열어줘라' 라는 것이다.

Interface Segregation Principle(ISP)은 코드의 coupling을 줄이고, 소프트웨어를 견고하게 만들며, 확장과 유지 보수를 더욱 쉽게 만들어준다.
ISP를 어기게되면, 사용자들은 사용하지 않은 인터페이스들에 의존하게되고, 이러한 코드는 새로운 기능을 추가하는게 불가능할 정도로
강하게 coupled 된다.

다음의 예제를 보도록 하자,

Mechanic은 정비공으로 자동차를 고칠 수 있다. 그래서 자동차에 대한 인터페이스를 갖고, 해당 인터페이스의 repair를 호출해주면 된다.
그런데, 정비공은 자동차만 고칠 수 있는 것은 아니다. 만약, 기차나, 비행기를 고친다면 어떻게 해야할까?? 그렇게된다면 Mechanic은 지금의 코드에서
기차, 비행기 클래스를 고치는 또 다른 메서드를 만들어야 한다. 이는 좋지 않은 경우인데, 개방-폐쇄 원칙을 어겼기 때문이다.

그렇다면 문제가 되는 것이 무엇일까?? Mechanic은 무슨 물건이 오던 간에 repair 메서드만을 볼 것이다. 즉, 이 인터페이스가 ICar든 전철이든 비행기든
상관없이 repair만 있으면 된다. 그런데, 지금의 예제는 ICar 인터페이스를 의존하고있는데, 이는 사용자(Mechanic)이 사용하지 않는 부분까지 들어가있어
강하게 연결되어버린 것이다(high coupling). 따라서, 우리는 Mechanic이 관심이어하는 인터페이스(기능)인 repair만 떼어내서 사용하도록 하자

다음과 같이 Mechanic이 관심있어하는 부분만 떼어서 인터페이스로 만드는 것이다. 이렇게 된다면, Mechanic은 자신이 고치려는 대상이 뭐든 간에 코드 변경없이 repair만 호출해주면 된다.

이처럼 인터페이스 분리원칙은 커다란 인터페이스에서, 하나하나 씩 기능 별로 작게 만들고, 구체적으로 만들어 사용자가 알아야만 하는 부분만 기능을 제공하는 것을 말한다.

추가적으로 다음과 같은 예제도 있다.

public interface ICar {
    void toggleDoorLock();
    void drive();
    void toggleHeater();
}

일반적인 자동차의 기능들을 인터페이스로 만들었다. 문을 열고, 운전하고, 히터를 켤 수 있다.

public class OldCar implements ICar{
    @Override
    public void toggleDoorLock() {

    }

    @Override
    public void drive() {

    }

    @Override
    public void toggleHeater() {

    }
}

OldCarICar를 구현하고 있는데, 사실 옛날 자동차 중에서는 문을 잠그는 기능, 히터를 켜는 기능이 없는 것도 존재한다. (군대 경험)
따라서, 해당 인터페이스는 잘못된 인터페이스이다. OldCar는 단순히 drive()라는 기능에만 의존하는데, toggleDoorLock, toggleHeater라는 기능과도
강하게 엮어져 쓸데없는 코드를 넣을 수 밖에 없다. 즉, 있지도 않은 기능을 구현해야한다는 것이다.

이러한 경우 가장 큰 문제는 자신과 관련 없는 기능의 이름이 변경되거나, 삭제, 추가될 때 영향을 받는다는 것이다. 가령, ICar에서 toggleDoorLock 기능을
삭제한다고 하자, OldCar는 별 관련도 없고, 사용도 안하는데 메서드를 삭제해야하는 수고스러움이 있다. 또한, ICarnavigation 메서드를 추가한다고하자,
OldCar에는 지원도 안하는데 메서드를 추가할 수 밖에 없다.

이처럼 해당 클래스가 관심없어하는 기능까지 강하게 연결되어있는 상태로 인해 코드의 수정이 일어나게 되고, 유지 보수가 어렵게 된다. 따라서, ISP에 맞춰서
클래스에서 관심있어 하는 기능만 인터페이스에서 떼어내서 구현하는 것이 좋다.

다음과 같이 수정하면 된다.

public interface Drivable {
    void drive();
}

public interface ICar extends Drivable {
    void drive();
}

public class OldCar implements ICar{
    @Override
    public void drive() {

    }
}

다음과 같이 ICar에는 가장 기본적인 자동차의 기능을 넣어준다. OldCar는 가장 기본적인 자동차의 기능인 dirve()에 의존하게 된다.
이제 toggleDoorLock 이나 toggleHeater은 새로운 기능 인터페이스를 만들고, 이를 다른 인터페이스에 만들거나, NewCar 클래스에 따로 넣어주면 된다.

5. Dependency Inversion Principle (의존 역전 원칙, DIP)

"High-level modules should not depend on low-level modules. Both should depend on abstractions"
"Abstractions should not depend on details, Details should depend on abstractions."

고수준의 모듈은 저수준의 모듈에 의존하지 않아야 하고, 둘 다 추상 모듈에 의존해야한다. 추상 모듈은 세부 모듈에 의존하지않아야하고, 세부 모듈은 추상 모듈에 의존해야한다.

우리는 특정 객체가 있다면 특정 객체에 관한 서비스는 해당 객체에서 처리되어야 한다고 생각한다. 가령, 자동차 정비소에서 서비스를 받는다면 자동차가
서비스를 받으므로
자동차가 서비스를 갖고있다. 따라서, 자동차는 서비스에 의존한다.

public class CarService {
    public void process()
    {
        System.out.print("hello");
    }
}

public class Car {
    CarService carService;

    public void setCarService(CarService carService) {
        this.carService = carService;
    }

    public void processCarService()
    {
        carService.process();
    }
}

class Main
{
    public static void main (String[] args)
    {
        Car car = new Car();
        car.setCarService(new CarService());
        car.processCarService();
    }
}

여기서 다음의 코드를 보자

car.setCarService(new CarService());
car.processCarService();

이렇게 자동차에 대한 서비스를 그때 그때 바꿔서 처리하면 될 것 같아보인다. 의미론적으로도 맞는 논리이기도 하다.

따라서, 자동차는 서비스에 의존한다는 맥락이 완성된다.

그러나, 생각해보면 자동차가 서비스에 의존한다는 의미론적으로 말이 맞지 않다. 서비스에 따라 차가 달라지는게 아니라, 차에 따라 서비스가
달라지는게 보통의 이치이기 때문이다.

코드 상으로도 마찬가지여야 한다. 위와 같이 자동차가 서비스에 의존해버리면, 자동차는 서비스가 어떤 것이냐에 따라 서비스를 처리받는다. 즉,
차가 어떤 것이냐에 따라 서비스를 받는 것이 아니라, 서비스에 따라 차가 받는 서비스가 달라지는 것이다. 이는 매우 어색하다.

따라서, 우리는 CarService와 Car의 의존 관계를 역전 시킬 것이다. 의미상으로는 자동차가 서비스를 받으니, 자동차가 서비스를 가져와 자동차 내부 상태를 변경하고 처리받도록 구현하는 것이
맞아보이지만, 이렇게되면 자동차가 서비스에 의존하는 의미론적으로, 코드적으로 매우 어색한 관계가 된다. 따라서, 이를 해결하기 위해 **고수준 모듈(자동차)이 저수준 모듈(서비스)에 의존하지 않도록 하는 것이다.

다음과 같이, CarService가 Car에 의존하도록 설계를 하면, 자동차 서비스는 자동차에 따라 다른 서비스를 제공할 수 있다. 언뜻보기에는 코드상으로 굉장히 어색해보이기도 한다.
자동차의 서비스를 만들어서 제공하는 것이 아닌, 자동차 클래스 내부에서 정의해서 제공해주기 때문이다. 그러나, 이러한 코드가 유지 보수에 굉장히 좋고, 어떤 자동차냐에 따라 서비스가 달라진다는
의미론적인 설계도 충족시킬 수 있다.

이것이 DIP의 첫번째 규칙인 고수준 모듈(Car)는 저수준 모듈(CarService)에 의존하지 않아야 한다 이다. 그리고 이와 같은 모습이 마치, 의존적 관계가 역전된 것 같아 보인다해서, 의존 역전이라고 한다.

여기서 끝난 것이 아니다. 지금은 단순히 Car 클래스에 대한 서비스만을 제공하였는데, 만약 구체적인 클래스인 Hyundai, BMW에 대한 서비스를 제공한다고하면 어떻게 해야할까??
CarService는 각 자동차 종류에 따른 서비스를 모두 들고 있어야 한다.

다음처럼 구현할 수 밖에 없게 된다. CarService는 자동차의 종류가 늘어날 때마다, 계속해서 맴버 변수와 메서드를 추가할 수 밖에 없다. 또한, BMW, Hyundai에 관한 의존관계가 너무 심하다.
만약, 이들이 service()메서드의 이름을 다른 것으로 바꾸거나, 매개변수를 추가하거나하면 CarService는 이에 맞춰 바꿔주는 수 밖에 없다.

Coupling은 의존 관계의 차수를 말한다. 가령, 위의 예제에서 CarServiceCoupling은 2이지만, 추가적으로 자동차 제조사가 늘어날 때마다 증가하게 된다.
Coupling 수를 줄이는 것을 Decoupling이라고 하는데, 이를 통해 의존 관계를 즐여야 한다. Coupling수가 줄어든다는 것은 의존관계가 줄어든다는 것이고, 이는 코드를
유지 보수하기 쉬워진다는 것이다. 즉, 적은 Coupling 수를 가지면 더 좋은 유지 보수를 할 수 있다는 것이다. 지금의 코드가 Coupling의 차수가 높은 이유는 구체 클래스가 너무 많은 구체 클래스에 의존해서 그렇다.

따라서, 우리는 추상 모듈(interface, abstract class)를 만들 것이다. 자동차에 대한 추상 클래스가 존재한다면, CarService는 어떤 자동차가 오던지 간에 신경쓰지 않고, 자동차가
내부적으로 구현한 service만 실행해주면 된다. 또한, 추상 클래스이기 때문에 메서드 이름이 바뀔 일도 거의 없다. 이렇게 되면, 구체 클래스가 구체 클래스에 의존하는 관계가 사라지고, 구체 클래스가 추상 클래스에 의존하는 관계가 만들어진다.

다음이 완성된 그림이다. ICar인터페이스(추상 모듈)을 만들고, 이를 구현하는 구현체 Hyundai, BMW내부에서 자신들이 받을 서비스를 구체화한다. 그 다음, CarServiceICar에 의존하여
Coupling을 줄이고, 자신이 제공해줄 서비스에만 집중할 수 있게 된다.

이렇게되면, 고수준 모듈(ICar)는 저수준 모듈(CarService)에 의존하지 않을 뿐더러, 저수준 모듈(CarService)는 추상 모듈(ICar)에 의존한다. 또한, 추상 모듈(ICar)는 구체 모듈(Hyudai, BMW)에 의존하지 않으며, 반대로
구체 모듈(Hyundai, BMW)가 추상 모듈(ICar)에 의존하게 된다. 추가적으로 추상 모듈(ICar)에 추가적으로 기능을 넣고 싶다면 추상 모듈을 만들어 의존하도록 하면된다. 그 반대로 추상 모듈이 구체 모듈에 의존하지 않도록만 하면 된다.

이것이 의존 역전 원칙이다. 우리가 일반적으로 생각하는 구현상의 관점과는 달리 의미론적으로 의존관계를 생각하고 구현하면 의존성의 관계가 역전되어 구현된다는 것이다. 의존 역전 원칙은
coupling을 줄이고, 유지 보수를 하는데 집중되어 있으므로 아주 중요한 원칙 중 하나이다.

이렇게 디자인 패턴의 기본 원칙이 끝났다. 앞으로는 여러가지 패턴들에 대해서 공부해볼 것이다.

0개의 댓글