백엔드에서 자주 사용되는 디자인패턴 -구조 패턴형-

jinhan han·2024년 8월 9일
0

spring과 java 개발

목록 보기
4/6
post-thumbnail

--- 구조 패턴 ----

깃허브 링크 : https://github.com/Jinhan-Han-Jeremy/RealDesignPattern

-------- 데코레이터(Decorator) --------

객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 하는 패턴

  • 데코레이터 패턴을 사용하는 이유
    • 전략 패턴을 사용하면 알고리즘의 변경에 따라서 코드 변경을 최소화 가능.
    • 객체 간의 결합도가 낮아져 유지보수와 테스트가 용이함.

자주 사용되는 경우 : 데이터 압축 라이브러리 (Data Compression Library), 데이터베이스 접근 라이브러리 (Database Access Library), 결제 처리 시스템 (Payment Processing System), 로깅 라이브러리 (Logging Library), UI 테마 변경 (UI Theme Switching), AI 알고리즘 선택 (AI Algorithm Selection), 비밀번호 해싱 (Password Hashing), 라우팅 알고리즘 (Routing Algorithm), 캐싱 전략 (Caching Strategy), 문서 포맷 변환 (Document Format Conversion)

데코레이터(Decorator) 장점

  • 데코레이터를 사용하면 서브클래스를 만들때보다 훨씬 더 유연하게 기능을 확장 가능.
  • 객체를 여러 데코레이터로 래핑하여 여러 동작을 결합 가능.
  • 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경 가능.
  • 구현체가 아닌 인터페이스를 바라봄으로써 의존 역전 원칙(DIP)Visit Website 준수
  • 각 장식자 클래스마다 고유의 책임을 가져 단일 책임 원칙(SRP)Visit Website을 준수
  • 기능 확장이 필요하면 장식자 클래스를 추가하면 되니 개방 폐쇄 원칙(OCP)Visit Website을 준수
  • 구현체가 아닌 인터페이스를 바라봄으로써 의존 역전 원칙(DIP)Visit Website 준수

데코레이터(Decorator) 단점

  • 만일 장식자 일부를 제거하고 싶다면, Wrapper 스택에서 특정 wrapper를 제거하는 것은 어려움.
  • 데코레이터를 조합하는 초기 생성코드가 보기 안좋을 수 있음. new A(new B(new C(new D())))
  • 어느 장식자를 먼저 데코레이팅 하느냐에 따라 데코레이터 스택 순서가 결정지게 되는데, 만일 순서에 의존하지 않는 방식으로 데코레이터를 구현하기는 어려움.

데코레이터 패턴 예제) 작업 진행 기록을 관리하는 인터페이스와 오브젝트 구성
1. public interface TaskService { : 서비스 기능들을 하는 함수들 인터페이스로 구성
2. public abstract class TaskServiceDecorator implements TaskService { : TaskService를 상속받고 추상 클래스로 선어
3. public TaskServiceDecorator(TaskService decoratedTaskService) { : 생성자 생성 및 인터페이스를 활용할 오브젝트에 값 할당
4. @Override public void assignTask(String task, String assignee) { : 서비스 기능들을 상속하고 호출하는 구조의 함수

package Decorator;
// TaskService 인터페이스: 작업 관리 서비스의 공통 인터페이스를 정의합니다.
public interface TaskService {
    void assignTask(String task, String assignee);
    void updateTaskStatus(String task, String status);
}
package Decorator;
// TaskServiceDecorator 추상 클래스: 작업 관리 서비스 데코레이터의 기본 구조를 정의합니다.
public abstract class TaskServiceDecorator implements TaskService {
    protected TaskService decoratedTaskService;
    public TaskServiceDecorator(TaskService decoratedTaskService) {
        this.decoratedTaskService = decoratedTaskService;
    }
    @Override
    public void assignTask(String task, String assignee) {
        decoratedTaskService.assignTask(task, assignee);
    }
    @Override
    public void updateTaskStatus(String task, String status) {
        decoratedTaskService.updateTaskStatus(task, status);
    }
}

데코레이터 패턴 예제) 작업 진행 기록들을 하고 이를 추적하는 클래스 구성
5. public class BasicTaskService implements TaskService { : TaskService 인터페이스를 상속하고 서비스가 작동하는 함수들을 구성하는 클래스 생성

package Decorator;
// BasicTaskService 클래스: 기본 작업 관리 서비스를 구현합니다.
public class BasicTaskService implements TaskService {
    @Override
    public void assignTask(String task, String assignee) {
        // 실제 작업 할당 로직
        System.out.println("Task '" + task + "' assigned to " + assignee);
    }
    @Override
    public void updateTaskStatus(String task, String status) {
        // 실제 작업 상태 업데이트 로직
        System.out.println("Task '" + task + "' status updated to " + status);
    }
}
  1. public class LoggingDecorator extends TaskServiceDecorator { : TaskServiceDecorator 클래스를 상속하고 따로 로그들을 남기는 기능들을 구성하는 클래스 생성
package Decorator;
// LoggingDecorator 클래스: 작업 관리 서비스에 로깅 기능을 추가하는 데코레이터를 구현합니다.
public class LoggingDecorator extends TaskServiceDecorator {
    public LoggingDecorator(TaskService decoratedTaskService) {
        super(decoratedTaskService);
    }
    @Override
    public void assignTask(String task, String assignee) {
        System.out.println("Logging: Assigning task '" + task + "' to " + assignee);
        super.assignTask(task, assignee);
        System.out.println("Logging: Task '" + task + "' assigned to " + assignee);
    }
    @Override
    public void updateTaskStatus(String task, String status) {
        System.out.println("Logging: Updating task '" + task + "' status to " + status);
        super.updateTaskStatus(task, status);
        System.out.println("Logging: Task '" + task + "' status updated to " + status);
    }
}
  1. public class NotificationDecorator extends TaskServiceDecorator { : TaskServiceDecorator 클래스를 상속하고 할당되거나 업데이트 된 작업을 알리는 기능들로 구성된 클래스 생성
  2. package Decorator;
    // NotificationDecorator 클래스: 작업 상태 업데이트 시 알림 기능을 추가하는 데코레이터를 구현합니다.
  3. @Override public void assignTask(String task, String assignee) { : 작업 기능들을 상속 후, 알람을 울리는 기능들을 구성
  4. private void notifyAssignee(String task, String message) { : 알람시에 구성하는 메세지 포맷을 구성
public class NotificationDecorator extends TaskServiceDecorator {
    public NotificationDecorator(TaskService decoratedTaskService) {
        super(decoratedTaskService);
    }
    @Override
    public void assignTask(String task, String assignee) {
        super.assignTask(task, assignee);
        notifyAssignee(task, assignee);
    }
    @Override
    public void updateTaskStatus(String task, String status) {
        super.updateTaskStatus(task, status);
        notifyAssignee(task, status);
    }
    private void notifyAssignee(String task, String message) {
        // 간단한 알림 로직 (예: 콘솔 출력)
        System.out.println("Notification: Task '" + task + "' - " + message);
    }
}

데코레이터 패턴 예제) 모든기능들을 이행하는 메인 클래스 구성

import Decorator.BasicTaskService;
import Decorator.LoggingDecorator;
import Decorator.NotificationDecorator;
import Decorator.TaskService;
public class MainByDecorator {
    public static void main(String[] args) {
        TaskService taskService = new BasicTaskService();
        // 작업 관리 서비스에 로깅 데코레이터 추가
        taskService = new LoggingDecorator(taskService);
        // 작업 관리 서비스에 상태 알림 데코레이터 추가
        taskService = new NotificationDecorator(taskService);
        // 작업 할당
        taskService.assignTask("Design Database Schema", "Alice");
        System.out.println();
        // 작업 상태 업데이트
        taskService.updateTaskStatus("Design Database Schema", "In Progress");
    }
}

전체적 구조 :

  • 데코레이터 클래스 { 상태없는 기본 생성자, 단일 인스턴스를 생성하고 사용하는 메서드, 서비스 메소드}


-------- 프록시(Proxy) --------

특정 객체를 직접 참조하지 않고 해당 객체를 대행(프락시)하는 객체를 통해 접근하는 패턴

  • 프록시 패턴을 사용하는 이유
    • 초기화 지연, 접근 제어, 로깅, 캐싱 등, 기존 객체 동작에 수정 없이 가미하고 싶을 때
    • 접근을 제어하거가 기능을 추가하고 싶은데, 기존의 특정 객체를 수정할 수 없는 상황일때
    • 객체지향 5원칙 중 하나인 OCP를 지키기 원할때
    • 객체지향 5원칙 중 하나인 SRP를 지키기 원할때
    • 유연한 코드 개발 가능

자주 사용되는 경우 : 지연 로딩 (Lazy Loading), 액세스 제어 (Access Control), 원격 프록시 (Remote Proxy), 캐싱 (Caching), 로깅 및 모니터링 (Logging and Monitoring), 트랜잭션 관리 (Transaction Management), 외부 서비스 연결 (External Service Connection), 원격 서비스 접근 (Remote Service Access)

프록시(Proxy) 장점

  • 개방 폐쇄 원칙(OCP)Visit Website 준수
    • 기존 대상 객체의 코드를 변경하지 않고 새로운 기능을 추가 가능.
  • 단일 책임 원칙(SRP)Visit Website 준수 
    • 대상 객체는 자신의 기능에만 집중 하고, 그 이외 부가 기능을 제공하는 역할을 프록시 객체에 위임하여 다중 책임을 회피 가능.
  • 원래 하려던 기능을 수행하며 그외의 부가적인 작업(로깅, 인증, 네트워크 통신 등)을 수행하는데 유용
  • 클라이언트는 객체를 신경쓰지 않고, 서비스 객체를 제어하거나 생명 주기를 관리 가능.
  • 프록시 객체는 실제 객체처럼 사용이 편함.

프록시(Proxy) 단점

  • 많은 프록시 클래스를 도입해야 하므로 코드의 복잡도가 증가.
    • 예를들어 여러 클래스에 로깅 기능을 가미 시키고 싶다면, 동일한 코드를 적용함에도 각각의 클래스에 해당되는 프록시 클래스를 만들어서 적용해야 되기 때문에 코드량이 많아지고 중복이 발생.
    • 자바에서는 리플렉션에서 제공하는 동적 프록시(Dynamic Proxy) 기법을 이용해서 해결 가능. (후술)
  • 프록시 클래스 자체에 들어가는 자원이 많다면 서비스로부터의 응답이 늦어질 수 있음.

프록시 종류: 기본형, 가상형, 보호, 로깅, 원격, 캐싱
https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%ED%94%84%EB%A1%9D%EC%8B%9CProxy-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

캐싱 프록시

  • 데이터가 큰 경우 캐싱하여 재사용을 유도
  • 클라이언트 요청의 결과를 캐시하고, 캐시의 주기 관리

캐싱 프록시 예제) 캐싱 프록시로 데이터를 캐싱에저장
1.public interface DatabaseService {: 쿼리를 캐시에 저장하는 인터페이스 사용.

package CachingProxy;
public interface DatabaseService {
    String queryDatabase(String query);
}

2.class RealDatabaseService implements DatabaseService{ : 데이터베이스 서비스 기능을 하는 클래스 구성.
3.public String queryDatabase(String query) { : 데이터베이스 서비스 기능을 하는 클래스 구성이며 지연시뮬레이션을 넣어 캐싱의 성능 테스트가 용이해짐.

package CachingProxy;
class RealDatabaseService implements DatabaseService{
    @Override
    public String queryDatabase(String query) {
        // Simulate a costly database query
        try {
            Thread.sleep(3000); // Simulate delay
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Result for query: " + query;
    }
}

4.public class CachingDatabaseProxy implements DatabaseService{ : 데이터베이스 서비스 기능을 하는 클래스 구성이며 지연시뮬레이션을 넣어 캐싱의 성능 테스트가 용이해짐.
5.String result = realService.queryDatabase(query); : 지연시뮬레이션을 시행하여 캐시에 데이터를 삽입.

package CachingProxy;
import java.util.HashMap;
import java.util.Map;
public class CachingDatabaseProxy implements DatabaseService{
    private RealDatabaseService realService = new RealDatabaseService();
    private Map<String, String> cache = new HashMap<>();
    @Override
    public String queryDatabase(String query) {
        if (!cache.containsKey(query)){
            // 실제 서비스에서 쿼리 결과를 가져와 캐시에 저장
            String result = realService.queryDatabase(query);
            cache.put(query, result);
        }
        else{
            // 캐시에 저장된 결과 반환
            System.out.println("Returning cached result for query: " + query);
        }
        return cache.get(query);
    }
}

캐싱 패턴 예제) 모든 기능들을 이행하는 메인 클래스 구성

import CachingProxy.CachingDatabaseProxy;
import CachingProxy.DatabaseService;
public class MainByCachingProxy {
    public static void main(String[] args) {
        DatabaseService service = new CachingDatabaseProxy();
        // First call - result is not cached
        System.out.println(service.queryDatabase("SELECT * FROM users"));
        // Second call - result should be cached
        System.out.println(service.queryDatabase("SELECT name FROM users"));
        // Third call - different query, result is not cached
        System.out.println(service.queryDatabase("SELECT type FROM orders"));
    }
}

전체적 구조 :

  • 인터페이스{서비스 함수 호출}
  • 지연 시뮬레이션 클래스 implements 인터페이스{@OVerride 서비스 함수}
  • 캐싱 프록시 클래스 implements 인터페이스{@OVerride 서비스 함수{지연시뮬레이션 서비스 함수 활용}}


원격 프록시

  • 프록시 클래스는 로컬에 있고, 대상 객체는 원격 서버에 존재하는 경우
  • 프록시 객체는 네트워크를 통해 클라이언트의 요청을 전달하여 네트워크와 관련된 불필요한 작업들을 처리하고 결과값만 반환

원격 프록시 예제) 원격 프록시로 데이터를 패칭
1.public interface DatabaseService {: 쿼리를 캐시에 저장하는 인터페이스 사용.

package RemoteProxy;
import java.rmi.Remote;
import java.rmi.RemoteException;
// Remote Interface
public interface RemoteService extends Remote {
    String fetchData(String param) throws RemoteException;
}

2.public class RealRemoteService extends UnicastRemoteObject implements RemoteService { : 리모트서비스 인터페이스 상속 및 UnicastRemoteObject 상속.
3.@Override public String fetchData(String param) throws RemoteException {: fetchData의 기능을 Override 및 활용.

package RemoteProxy;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// RealSubject
public class RealRemoteService extends UnicastRemoteObject implements RemoteService {
    protected RealRemoteService() throws RemoteException {
        super();
    }
    @Override
    public String fetchData(String param) throws RemoteException {
        // Simulate fetching data from a remote server
        return "Data from server for " + param;
    }
}

4.public class RemoteServiceProxy implements RemoteService { : RemoteService를 상속 및 생성 자와 서비스 기능 추가.
5.@Override public String fetchData(String param) throws RemoteException { : fetchData로 재귀 형태로 서비스 기능을 활용

package RemoteProxy;
import java.rmi.RemoteException;
// Proxy
public class RemoteServiceProxy implements RemoteService {
    private final RemoteService realService;
    public RemoteServiceProxy(RemoteService realService) {
        this.realService = realService;
    }
    @Override
    public String fetchData(String param) throws RemoteException {
        System.out.println("Proxy: Fetching data for " + param);
        return realService.fetchData(param);
    }
}

원격 프록시 예제) 서버 기능을 실행하여, 원격이 가능하게함

package RemoteProxy;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
// Server
public class Server {
    public static void main(String[] args) {
        try {
            RealRemoteService realService = new RealRemoteService();
            Registry registry = LocateRegistry.createRegistry(1099);
            registry.bind("RemoteService", realService);
            System.out.println("Server started");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

원격 프록시 예제) 모든 기능들을 이행하는 메인 클래스 구성

import RemoteProxy.RemoteService;
import RemoteProxy.RemoteServiceProxy;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class MainByRemoteProxy {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.getRegistry("localhost", 1099);
            RemoteService realService = (RemoteService) registry.lookup("RemoteService");
            RemoteServiceProxy proxy = new RemoteServiceProxy(realService);
            // Fetch data through the proxy         System.out.println(proxy.fetchData("test1"));
            System.out.println(proxy.fetchData("test2"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

전체적 구조 :

  • 인터페이스{서비스 함수 호출}
  • 원격 기능 클래스 implements 인터페이스{기본 생성자, @OVerride 서비스 함수}
  • 원격 프록시 클래스 implements 인터페이스{생성자(변수들), @OVerride 서비스 함수 throws RemoteException{서비스 기능들}}
  • 서버 클래스{ 원격 기능 서비스들 호출}


-------- 퍼사드(Facade) --------

서브시스템의 인터페이스 집합들에 하나의 통합된 인터페이스를 제공하는 패턴

  • 퍼사드 패턴을 사용하는 이유
    • 복잡한 시스템의 간단한 인터페이스가 필요한 경우
    • 간단한 인터페이스를 통해 복잡한 시스템을 접근할때, 시스템의 결합도를 줄이고 유연성을 높이는 경우
    • 서브 시스템을 노출하지 않고 사용자 인터페이스를 제공하는 경우
    • 시스템을 사용하고 있는 외부와 결합도가 너무 높을 때 의존성 낮추기 위할때

자주 사용되는 경우 : 서브시스템의 단순화 (Simplification of Subsystems), 외부 API와의 통합 (Integration with External APIs), 크로스커팅 관심사 처리 (Handling Cross-Cutting Concerns), 레거시 코드와의 통합 (Integration with Legacy Code), 테스트 용이성 향상 (Improving Testability), 사용자 인증 (User Authentication), 결제 시스템 (Payment System)

퍼사드(Facade) 장점

  • 하위 시스템의 복잡성에서 코드를 분리하여, 외부에서 시스템을 사용하기 쉬움.
  • 하위 시스템 간의 의존 관계가 많을 경우 이를 감소시키고 의존성을 한 곳으로 모을 수 있음.
  • 복잡한 코드를 감춤으로써, 클라이언트가 시스템의 코드를 모르더라도 Facade 클래스만 이해하고 사용 가능. 

퍼사드(Facade) 단점

  • 퍼사드가 앱의 모든 클래스에 결합된 God 객체가 될 수 있음.
  • 퍼사드 클래스 자체가 서브시스템에 대한 의존성을 가지게 되어 의존성을 완전히는 피하는건 불가능.
  • 어찌되었건 추가적인 코드가 늘어나는 것이기 때문에 유지보수 측면에서 공수가 더 많이 들게 됨.
  • 따라서 추상화 하고자하는 시스템이 얼마나 복잡한지 퍼사드 패턴을 통해서 얻게 되는 이점과 추가적인 유지보수 비용을 비교해보며 결정해야 함.

퍼사드 예제) DBMS 시스템 재구성 프로그램
퍼사드 예제) 클래스들의 구성 Row,Cache,DBMS,Message
1. class Row { : 데이터에 저장되는 형식 정보

package Facade;
class Row  {
    private String name;
    private String birthday;
    private String email;
    public Row(String name, String birthday, String email) {
        this.name = name;
        this.birthday = birthday;
        this.email = email;
    }
    public String getName() {
        return name;
    }
    public String getBirthday() {
        return birthday;
    }
    public String getEmail() {
        return email;
    }
}
  1. class DBMS { : 데이터베이스 저장 및 호출.
package Facade;
import java.util.HashMap;
// 데이터베이스 역할을 하는 클래스
class DBMS {
    private HashMap<String, Row> db = new HashMap<>();
    public void put(String name, Row row) {
        db.put(name, row);
    }
    // 데이터베이스에 쿼리를 날려 결과를 받아오는 메소드
    public Row query(String name) {
        try {
            Thread.sleep(500); // DB 조회 시간을 비유하여 0.5초대기로 구현
        } catch (InterruptedException e) {
        }
        return db.get(name.toLowerCase());
    }
}
  1. class Cache { : 캐시에 저장 및 호출.
package Facade;
import java.util.HashMap;
// 데이터베이스 역할을 하는 클래스
class DBMS {
    private HashMap<String, Row> db = new HashMap<>();
    public void put(String name, Row row) {
        db.put(name, row);
    }
    // 데이터베이스에 쿼리를 날려 결과를 받아오는 메소드
    public Row query(String name) {
        try {
            Thread.sleep(500); // DB 조회 시간을 비유하여 0.5초대기로 구현
        } catch (InterruptedException e) {
        }
        return db.get(name.toLowerCase());
    }
}
  1. class Message { : 데이터를 출력하는 구성.
package Facade;
// Row 클래스를 보기좋게 출력하는 클래스
class Message {
    private Row row;
    public Message(Row row) {
        this.row = row;
    }
    public String makeName() {
        return "Name : \"" + row.getName() + "\"";
    }
    public String makeBirthday() {
        return "Birthday : " + row.getBirthday();
    }
    public String makeEmail() {
        return "Email : " + row.getEmail();
    }
}
  1. public class Facade { : 파사드 패턴으로 클래스들 호출 구성.
  2. public void insert() { : 데이터 입력.
  3. public void run(String name) { : 데이터 결과 출력.
package Facade;
public class Facade {
    private DBMS dbms = new DBMS();
    private Cache cache = new Cache();
    public void insert() {
        dbms.put("홍길동", new Row("홍길동", "1890-02-14", "honggildong@naver.com"));
        dbms.put("임꺽정", new Row("임꺽정", "1820-11-02", "imgguckjong@naver.com"));
        dbms.put("주몽", new Row("주몽", "710-08-27", "jumong@naver.com"));
    }
    public void run(String name) {
        Row row = cache.get(name);
        // 1. 만약 캐시에 없다면
        if (row == null){
            row = dbms.query(name); // DB에 해당 데이터를 조회해서 row에 저장하고
            if(row != null) {
                cache.put(row); // 캐시에 저장
            }
        }
        // 2. dbms.query(name)에서 조회된 값이 있으면
        if(row != null) {
            Message message = new Message(row);
            System.out.println(message.makeName());
 			System.out.println(message.makeBirthday());
 			System.out.println(message.makeEmail());
        }
        // 3. 조회된 값이 없으면
        else {
            System.out.println(name + " 가 데이터베이스에 존재하지 않습니다.");
        }
    }
}

파사드 패턴 예제) 모든 기능들을 이행하는 메인 클래스 구성

import Facade.*;
public class MainByFacade {
    public static void main(String[] args) {
        // 1. 퍼사드 객체 생성
        Facade facade = new Facade();
        // 2. db 값 insert
        facade.insert();
        // 3. 퍼사드로 데이터베이스 & 캐싱 & 메세징 로직을 한번에 조회
        String name = "홍길동";
        facade.run(name);
    }
}

전체적 구조 :

  • 구성 클래스들 (상황마다 구성 클래스들 마다 다름)
  • 퍼사드 클래스 { 필요 객체들 호출, 필요 기능들을 구성}


참조 :
https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%B6%94%EC%83%81-%ED%8C%A9%ED%86%A0%EB%A6%ACAbstract-Factory-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90#%EC%B6%94%EC%83%81_%ED%8C%A9%ED%86%A0%EB%A6%AC_%ED%8C%A8%ED%84%B4_%ED%8A%B9%EC%A7%95

https://innovation123.tistory.com/9#%EC%8B%B1%EA%B8%80%ED%86%A4%EC%9D%98%20%EB%AC%B8%EC%A0%9C%20%ED%95%B4%EA%B2%B0%20-%20statelsess-1

https://inpa.tistory.com/entry/GOF-💠-템플릿-메소드Template-Method-패턴-제대로-배워보자

https://dev-coco.tistory.com/177

https://appleg1226.tistory.com/category/Study?page=2

https://blog.naver.com/jvioonpe/220247760303

profile
개발자+분석가+BusinessStrategist

0개의 댓글