구조 패턴이란: 구조패턴(structural patterns)은 클래스나 객체를 조합해 더 큰 구조를 만드는 패턴이다.
예를 들어 서로 다른 인터페이스를 지닌 2개의 객체를 묶어 단일 인터페이스를 제공하거나 객체들을 서로 묶어 새로운 기능을 제공하는 패턴이다. 이 패턴을 사용하면 서로 독립적으로 개발한 클래스 라이브러리를 마치 하나인 것처럼 사용할 수 있다.
또, 여러 인터페이스를 합성하여 서로 다른 인터페이스들의 통일된 추상을 제공한다. 가장 중요한 점은 인터페이스나 구현을 복합하는 것이 아니라 객체를 합성하는 방법을 제공한다는 것이다.
public class LoginHandler {
UserDetailsService userDetailsService;
public LoginHandler(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public String login(String username, String password) {
UserDetails userDetails = userDetailsService.loadUser(username);
if (userDetails.getPassword().equals(password)) {
return userDetails.getUsername();
} else {
throw new IllegalArgumentException();
}
}
}
클라이언트 코드는 UserDetailsService, UserDetails 타입으로 로직을 수행하기 때문에 AccountService, Account 타입의 객체를 클라이언트 코드에서 쓸 수 있게 타입을 UserDetailsService, UserDetails 로 바꿔줘야 한다.
public class Account {
private String name;
private String password;
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
public class AccountService {
public Account findAccountByUsername(String username) {
Account account = new Account();
account.setName(username);
account.setPassword(username);
account.setEmail(username);
return account;
}
public void createNewAccount(Account account) {
}
public void updateAccount(Account account) {
}
}
adaptee 객체를 adaptor 를 통해서 클라이언트 코드에서 원하는 타입으로 바꿔보자!
public class AccountUserDetails implements UserDetails {
private Account account;
public AccountUserDetails(Account account) {
this.account = account;
}
@Override
public String getUsername() {
return account.getName();
}
@Override
public String getPassword() {
return account.getPassword();
}
}
public class AccountUserDetailsService implements UserDetailsService {
private AccountService accountService;
public AccountUserDetailsService(AccountService accountService) {
this.accountService = accountService;
}
@Override
public UserDetails loadUser(String username) {
return new AccountUserDetails(accountService.findAccountByUsername(username));
}
}
public class App {
public static void main(String[] args) {
AccountService accountService = new AccountService();
UserDetailsService userDetailsService = new AccountUserDetailsService(accountService);
LoginHandler loginHandler = new LoginHandler(userDetailsService);
String login = loginHandler.login("keesun", "keesun");
System.out.println(login);
}
}
어댑터 패턴을 이용하여 한 클래스의 인터페이스를 클라이언트에서 사용하고자하는 다른 인터페이스로 변환하였다.
리그오브레전드의 챔피언(구현)과 스킨(기능)을 통해 설명하겠다.
챔피언 마다 착용하는 스킨이 여러가지인데 상속을 사용하면 KDA아리, KDA카이사, 풀파티 아리, 풀파티 카이사 등 챔피언과 스킨이 한 클래스에 섞여 있다.(SRP 위반)
챔피언이 100개, 스킨이 100개 일 경우 100 X 100 의 클래스를 정의하여야 한다.
이를 해결하기 위해 구현부에 해당하는 챔피언에서 추상적인 스킨을 분리한다.
public interface Champion {
void name();
void move();
void skillQ();
void skillW();
void skillE();
void skillR();
}
public class DefaultChampion implements Champion {
private Skin skin;
private String name;
public DefaultChampion(Skin skin, String name) {
this.skin = skin;
this.name = name;
}
@Override
public void name() {
System.out.println(skin.getName() + " " + this.name);
}
@Override
public void move() {
System.out.printf("%s %s move\n", skin.getName(), this.name);
}
@Override
public void skillQ() {
System.out.printf("%s %s Q\n", skin.getName(), this.name);
}
@Override
public void skillW() {
System.out.printf("%s %s W\n", skin.getName(), this.name);
}
@Override
public void skillE() {
System.out.printf("%s %s E\n", skin.getName(), this.name);
}
@Override
public void skillR() {
System.out.printf("%s %s R\n", skin.getName(), this.name);
}
}
public class 아칼리 extends DefaultChampion {
public 아칼리(Skin skin) {
super(skin, "아칼리");
}
}
public class 아리 extends DefaultChampion {
public 아리(Skin skin) {
super(skin, "아리");
}
}
public interface Skin {
String getName();
}
public class KDA implements Skin {
@Override
public String getName() {
return "KDA";
}
}
public class PoolParty implements Skin {
@Override
public String getName() {
return "풀파티";
}
}
public class App {
public static void main(String[] args) {
//기능 추가
Skin poolParty = new PoolParty();
Skin KDA = new KDA();
//구현 추가
Champion ahri = new 아리(poolParty);
Champion akali = new 아칼리(KDA);
ahri.name();
ahri.move();
ahri.skillQ();
ahri.skillW();
akali.name();
}
}
구현부에 해당하는 챔피언에서 추상층에 해당하는 스킨을 분리하여 각자 독립적으로 변형이 가능하고 확장이 가능하게 하였다.
public class Client {
public static void main(String[] args) {
Item doranBlade = new Item("도란검", 450);
Item healPotion = new Item("체력 물약", 50);
Bag bag = new Bag();
bag.add(doranBlade);
bag.add(healPotion);
Client client = new Client();
client.printPrice(doranBlade);
client.printPrice(bag);
}
private void printPrice(Component item) {
System.out.println(item.getPrice());
}
}
public interface Component {
int getPrice();
}
클라이언트 코드에서 getPrice() 를 필요로 하기 때문에 Item 객체와 Bag 객체 둘다 getPrice() 를 오버라이딩 한다.
public class Item implements Component {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public int getPrice() {
return this.price;
}
}
public class Bag implements Component {
private List<Component> components = new ArrayList<>();
public void add(Component item) {
components.add(item);
}
public List<Component> getComponents() {
return components;
}
@Override
public int getPrice() {
return components.stream().mapToInt(Component::getPrice).sum();
}
}
Bag 은 Component 객체를 내부에 저장할 수 있는 복합 객체이기 때문에 stream 을 활용하여 전체를 더한 값을 반환한다.
public class Client {
private CommentService commentService;
public Client(CommentService commentService) {
this.commentService = commentService;
}
public void writeComment(String comment) {
commentService.addComment(comment);
}
public static void main(String[] args) {
//기본적인 동작을 수행
CommentService commentService = new DefaultCommentService();
commentService = new SpamFilteringCommentDecorator(new TrimmingCommentDecorator(commentService));
Client client = new Client(commentService);
client.writeComment("오징어게임");
client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
client.writeComment("http://whiteship.me");
}
}
public interface CommentService {
void addComment(String comment);
}
public class DefaultCommentService implements CommentService {
@Override
public void addComment(String comment) {
System.out.println(comment);
}
}
기본동작을 하는 객체.
public class SpamFilteringCommentDecorator extends CommentDecorator {
public SpamFilteringCommentDecorator(CommentService commentService) {
super(commentService);
}
@Override
public void addComment(String comment) {
if (isNotSpam(comment)) {
super.addComment(comment);
}
}
private boolean isNotSpam(String comment) {
return !comment.contains("http");
}
}
public class TrimmingCommentDecorator extends CommentDecorator {
public TrimmingCommentDecorator(CommentService commentService) {
super(commentService);
}
@Override
public void addComment(String comment) {
super.addComment(trim(comment));
}
private String trim(String comment) {
return comment.replace("...", "");
}
}
기본 동작을 꾸며주는 decorator 객체
public class Client {
public static void main(String[] args) {
String to = "keesun@whiteship.me";
String from = "whiteship@whiteship.me";
String host = "127.0.0.1";
Properties properties = System.getProperties();
properties.setProperty("mail.smtp.host", host);
Session session = Session.getDefaultInstance(properties);
try {
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(from));
message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
message.setSubject("Test Mail from Java Program");
message.setText("message");
Transport.send(message);
} catch (MessagingException e) {
e.printStackTrace();
}
}
}
public class EmailMessage {
private String from;
private String to;
private String cc;
private String bcc;
private String subject;
private String text;
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getCc() {
return cc;
}
public void setCc(String cc) {
this.cc = cc;
}
public String getBcc() {
return bcc;
}
public void setBcc(String bcc) {
this.bcc = bcc;
}
}
이메일에 보낼 메시지를 저장할 객체
public class EmailSettings {
private String host;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
}
이메일을 보내기 위한 세팅정보를 저장할 객체
public class EmailSender {
private EmailSettings emailSettings;
public EmailSender(EmailSettings emailSettings) {
this.emailSettings = emailSettings;
}
/**
* 이메일 보내는 메소드
* @param emailMessage
*/
public void sendEmail(EmailMessage emailMessage) {
Properties properties = System.getProperties();
properties.setProperty("mail.smtp.host", emailSettings.getHost());
Session session = Session.getDefaultInstance(properties);
try {
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(emailMessage.getFrom()));
message.addRecipient(Message.RecipientType.TO, new InternetAddress(emailMessage.getTo()));
message.addRecipient(Message.RecipientType.CC, new InternetAddress(emailMessage.getCc()));
message.setSubject(emailMessage.getSubject());
message.setText(emailMessage.getText());
Transport.send(message);
} catch (MessagingException e) {
e.printStackTrace();
}
}
}
이메일을 보내기 위한 객체
public class Client {
public static void main(String[] args) {
EmailSettings emailSettings = new EmailSettings();
emailSettings.setHost("127.0.0.1");
EmailSender emailSender = new EmailSender(emailSettings);
EmailMessage emailMessage = new EmailMessage();
emailMessage.setFrom("keesun");
emailMessage.setTo("whiteship");
emailMessage.setCc("일남");
emailMessage.setSubject("오징어게임");
emailMessage.setText("밖은 더 지옥이더라고..");
emailSender.sendEmail(emailMessage);
}
}
퍼사드 패턴 적용 후 코드가 더 깔끔해진 것을 확인할 수 있다.
Character c1 = new Character('h', "white", "Nanum", 12);
Character c2 = new Character('e', "white", "Nanum", 12);
Character c3 = new Character('l', "white", "Nanum", 12);
Character c4 = new Character('l', "white", "Nanum", 12);
Character c5 = new Character('o', "white", "Nanum", 12);
위의 코드처럼 Character 객체를 만들 경우 글자 색, 폰트, 글자 크기 반복하여 사용하는 것을 알 수 있다.
플라이웨이트 패턴을 적용하여 메모리를 아껴보자!
public class Character {
private char value;
private String color;
private String fontFamily;
private int fontSize;
public Character(char value, String color, String fontFamily, int fontSize) {
this.value = value;
this.color = color;
this.fontFamily = fontFamily;
this.fontSize = fontSize;
}
}
public final class Font {
final String family;
final int size;
public Font(String family, int size) {
this.family = family;
this.size = size;
}
public String getFamily() {
return family;
}
public int getSize() {
return size;
}
}
Character 객체의 폰트와, 글자 크기를 저장할 객체이다. 불변 객체이기 때문에 상속을 막기 위해 클래스와 필드를 final로 하였다.
public class FontFactory {
private Map<String, Font> cache = new HashMap<>();
public Font getFont(String font) {
if (cache.containsKey(font)) {
return cache.get(font);
} else {
String[] split = font.split(":");
Font newFont = new Font(split[0], Integer.parseInt(split[1]));
cache.put(font, newFont);
return newFont;
}
}
}
들어오는 값을 split 하여 font 객체를 만든다. 팩토리 내부에 map 을 두어 값이 있을 경우 map에 저장되어 있는 값을 반환하고, 없을 경우 새로 만들어 map 에 저장하여 반환한다.
프록시 객체든, 타겟 객체든 둘다 같은 타입으로 추상화하여야 사용하는 클라이언트 코드에서 내부 구현을 모른체 사용이 가능하다.
public interface GameService {
void startGame();
}
타입추상화를 위해 정의하며, 클라이언트 코드에서 사용할 메서드를 정의한다.
public class GameServiceProxy implements GameService {
private GameService gameService;
@Override
public void startGame() {
long before = System.currentTimeMillis();
if (this.gameService == null) {
this.gameService = new DefaultGameService();
}
gameService.startGame();
System.out.println(System.currentTimeMillis() - before);
}
}
프록시 객체로 대리자 역할을 하는데, target 객체를 호출하기 전이나 후로 추가적인 기능이 가능하며, 위 코드는 지연로딩을 활용하였다.
public class DefaultGameService implements GameService {
@Override
public void startGame() {
System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
}
}
실제 클라이언트 코드에 기능을 제공할 겍체이다.
public class Client {
public static void main(String[] args) {
GameService gameService = new GameServiceProxy();
gameService.startGame();
}
}
클라이언트 코드는 proxy 객체든 real 객체든 타입이 같기 때문에 좀 더 유연한 학장이 가능하다.