-
Java — 모범 사례 및 권장 사항: 디자인 패턴JAVA 2022. 1. 8. 11:12반응형
출처: https://blog.singular.uk/java-good-practices-and-recommendations-design-patterns-eade30be7965
설계 패턴은 소프트웨어 개발 중에 자주 발생하는 문제에 대한 일반적인 해결책이다. 이러한 솔루션은 우아하고 대부분의 경우 객체 생성, 리소스 할당, 코드 단순화 등과 관련된 다양한 문제를 해결하는 가장 효과적인 방법을 제공한다. 비즈니스 논리에 따라 솔루션 자체를 커스터마이징할 필요가 있는 반면, 그것들이 주어지는 맥락을 유지할 필요가 있다.
설계 패턴은 세 가지 범주로 구분된다.
- 창조, 객체 생성 중에 발생하는 다양한 문제를 해결하기 위한 솔루션 제공
- 구조, 더 큰 구조에서 클래스를 구성할 수 있는 방법을 찾아냄으로써 인스턴스화 문제에 대한 해결책 제공
- 행동, 코드의 분리된 부분들 사이의 의사소통에서 발생하는 문제에 대한 해결책을 제시하라.
설계 패턴 중 일부는 DAO 설계 패턴의 경우와 같이 실제로 아키텍처 설계 중에 지침으로 사용될 수 있다. 소프트웨어 아키텍처는 대개 세 개의 계층을 가지고 있다: 앱의 엔드포인트, 서비스 계층(즉 비즈니스 로직)과 데이터 계층.
DAO
데이터 계층은 데이터베이스와 통신하는 부분을 애플리케이션의 나머지 부분과 분리하는 DAO 설계 패턴(Data Access Object)을 사용하여 구현된다. DAO 패턴은 모든 엔티티에 대한 CRUD(생성, 읽기, 업데이트, 삭제) 작업을 정의한다. 기업 자체에 자주 사용될 명명된/원래 질의를 추가함으로써 지속성 계층을 완전히 분리할 수 있다.
public interface DAO<T,E extends Serializable>{ public T save(T object); public Boolean delete(T object); public T update(T object); public T find(E id); }
DAO 자체의 인터페이스는 구현에서 지정해야 하는 Operation만을 정의한다. 구현 자체는 제공된 엔터티 매니저와 함께 일반적인 유형을 사용한다. 엔티티 매니저는 앱의 모든 지속적 운영을 관리하는 클래스로서 애플리케이션 컨텍스트를 통해 얻을 수 있다.
public abstract class GenericDAO<T,E> implements DAO<T,E>{ @PersistenceContext private EntityManager entityManager; public T save(T object){ return entityManager.persist(object); } public T find(E id){ return entityManager.find(T.class,id); } public Boolean delete(T object){ return entityManager.remove(object); } public T update(T object){ return entityManager.merge(object); } }
제공된 예제는 Hibernate와 자바의 persistence에 대한 기본적인 이해가 필요하다. Hibernate란 ORM 도구(object relational mapping)로 자바 코드에서 테이블을 생성하고 쿼리 입력 및 실행에 HQL(hibernate query language)을 사용한다.
@Entity @Table(name="person") @NamedQueries ( { @NamedQuery(name=Person.GET_PERSON_BY_AGE,query="Select * from User u where u.age>:age") } ) public class Person{ public static final String GET_PERSON_BY_AGE = "Person.getPersonByAge"; @Id @GeneratedValue( strategy = GenerationType.IDENTITY) @Column(name="id",unique="true") public int id; @Column(name="name") public String name; public Person(String name){ this.name=name; } //getters and setters... }
DAO 클래스는 기본 CRUD 연산을 구현하는 일반적인 DAO를 확장하므로 사용할 특정 쿼리만 추가하면 된다.
public PersonDAO extends GenericDAO<Person,Integer>{ public List<Person> getPersonByAge(int age){ Query q=entityManager.createNamedQuery(Person.GET_PERSON_BY_AGE, Person.class); q.setParameter("age",5); return (List<Person>)q.getResultList(); } }
찬성:
- 코드의 논리적 분리 및 물리적 분리를 모두 제공하며, 이는 비즈니스 논리를 구현하기 쉽게 한다.
- DAO 클래스는 캐시 전략으로 쉽게 확장할 수 있으며, 이 전략은 메소드에서 구현될 수 있다.
- DAO 클래스가 EJB로 선언된 경우, 각 메소드는 기본 트랜잭션의 범위를 제어하기 위해 트랜잭션 속성을 지정할 수 있다.
반대:
- DAO 객체는 일반적으로 전체 개체를 처리하므로 데이터베이스와의 연결에 오버헤드가 발생한다. 저장 작업의 경우 전체 객체가 한번에 저장되어 이점이 있지만, 읽기 작업은 비용이 많이 들 수 있다.
- 이를 방지하기 위해 비즈니스 요구에 따라 객체의 작은 부분을 검색하기 위해 네이티브 또는 명명된 쿼리를 사용해야 한다.
- DAO 패턴은 장점이 경미하고 코드가 더 복잡해지기 때문에 작은 앱에서 사용해서는 안 된다.
Factory
설계 패턴은 종종 큰 코드 덩어리를 단순화하거나 애플리케이션 흐름에서 특정 구현을 숨기기 위해 사용된다. 이러한 종류의 문제들에 대한 완벽한 예는 factory 디자인 패턴인데, 이것은 정확한 클래스를 지정하지 않고도 객체를 만들 수 있는 창조적인 디자인 패턴이다. 슈퍼 클래스와 슈퍼 클래스에서 상속되는 여러개의 서브 클래스를 활용하도록 제안한다. 실행 중에는 슈퍼 클래스만 사용되며 그 값은 factory 클래스에 따라 달라진다.
public class Car{ private String model; private int numberOfDoors; public Car(){ } public String getModel(){ return this.model; } public int getNumberOfDoors(){ return this.numberOfDoors; } public void setModel(String model){ this.model = model; } public void setNumberOfDoors(int n){ this.numberOfDoors = n; } } public class Jeep extends Car{ private boolean land; public Jeep(){ } public void setLand(boolean land){ this.land=land; } public boolean getLand(){ return this.land; } } public class Truck extends Car{ private float capacity; public Truck(){ } public void setCapacity(float capacity){ this.capacity=capacity; } public float getCapacity(){ return this.capacity; } }
이 패턴을 이용하기 위해서는 주어진 입력에 대해 올바른 서브 클래스를 반환하는 factory 클래스를 구현해야 한다. 위의 자바 클래스는 하나의 슈퍼 클래스(Car.java)와 두 개의 서브 클래스(Truck.java 및 Jeep.java)를 지정한다. 구현에서 Car 클래스의 대상을 인스턴스화하며, arguments에 따라 factory 클래스가 Jeep인지 Truck인지 여부를 결정할 것이다.
public class CarFactory{ public Car getCarType(int numberOfDoors, String model, Float capacity, Boolean land){ Car car=null; if(capacity!=null){ car=new Jeep(); //implement setters }else{ car=new Truck(); //implement setters } return car; } }
런타임 동안 factory 클래스는 입력을 고려하여 올바른 서브 클래스를 인스턴스화한다.
public static void main(String[] args){ Car c=null; CarFactory carFactory=new CarFactory(); c = carFactory.getCarType(2,"BMW",null, true); }
Abstract factory 디자인 패턴은 같은 방식으로 작동하지만 일반적인 클래스 대신 부모 클래스가 추상 클래스이다. 추상 클래스는 기본적으로 비어 있기 때문에 일반적으로 더 빠르고 인스턴스화 하기가 쉽다. 구현은 모든 메소드들과 함께 부모 클래스만이 추상적으로 선언되며 서브 클래스는 추상계급에서 선언된 메소드의 행동을 구현해야 하는 것과 동일하다.
Abstract factory의 예는 인터페이스를 사용하여 생성된다. 인터페이스를 추상적인 클래스로 대체하는 것만으로 동일한 작업을 할 수 있으며, 인터페이스를 구현하는 대신 하위 클래스가 추상 클래스를 확장한다.
public interface Car { public String getModel(); public Integer getNumberOfDoors(); public String getType(); } public class Jeep implements Car{ private String model; private Integer numberOfDoors; private Boolean isLand; public Jeep() {} public Jeep(String model, Integer numberOfDoors, Boolean isLand){ this.model = model; this.numberOfDoors = numberOfDoors; this.isLand = isLand; } public String getModel(){ return model; } public Integer getNumberOfDoors() { return numberOfDoors; } public Boolean isLand() { return isLand; } public void setLand(Boolean isLand) { this.isLand = isLand; } public void setModel(String model) { this.model = model; } public void setNumberOfDoors(Integer numberOfDoors){ this.numberOfDoors = numberOfDoors; } public String getType(){ return "jeep"; } } public class Truck implements Car{ private String model; private Integer numberOfDoors; private Integer numberOfWheels; public Truck(String model, Integer numberOfDoors, Integer numberOfWheels) { this.model = model; this.numberOfDoors = numberOfDoors; this.numberOfWheels = numberOfWheels; } public Truck() {} public String getModel() { return model; } public Integer getNumberOfDoors() { return numberOfDoors; } public Integer getNumberOfWheels() { return numberOfWheels; } public void setNumberOfWheels(Integer numberOfWheels) { this.numberOfWheels = numberOfWheels; } public void setModel(String model) { this.model = model; } public void setNumberOfDoors(Integer numberOfDoors) { this.numberOfDoors = numberOfDoors; } public String getType(){ return "truck"; } } public class CarFactory { public CarFactory(){} public Car getCarType(String model,Integer numberOfDoors, Integer numberOfWheels, Boolean isLand){ if(numberOfWheels==null){ return new Jeep(model,numberOfDoors,isLand); }else{ return new Truck(model,numberOfDoors,numberOfWheels); } } }
유일한 차이점은 추상적인 클래스에서 선언된 메소드들은 각각의 하위 클래스에서 실행되어야 한다는 것이다. factory와 주요 메소드는 두 경우 모두 동일하다.
public class CarMain { public static void main(String[] args) { Car car=null; CarFactory carFactory=new CarFactory(); car=carFactory.getCarType("Ford", new Integer(4), null, new Boolean(true)); System.out.println(car.getType()); } }
출력은 다음과 같다.
찬성:
- 느슨한 결합과 높은 수준의 추상화를 허용한다.
- 확장 가능하며 특정 구현을 애플리케이션에서 분리하는 데 사용할 수 있다.
- 적절한 인스턴스화 로직을 추가하기만 하면 계층 하위에 새로운 클래스를 만든 후 factory 클래스를 재사용할 수 있으며 코드는 여전히 작동한다.
- 슈퍼 클래스를 사용하여 모든 시나리오를 포괄하는 것이 간단하기 때문에 단위 테스트가 쉽다.
반대:
- 종종 너무 추상적이고 이해하기 어렵다.
- 소형 애플리케이션에서는 객체 생성 중에 오버헤드(더 많은 코드)만 발생하므로 factory 설계 패턴을 언제 구현해야 하는지가 매우 중요하다.
- factory 설계 패턴은 컨텍스트를 유지해야 한다. 즉, 동일한 상위 클래스에서 상속받거나 동일한 인터페이스를 구현하는 클래스만 factory 설계 패턴에 적용할 수 있다.
Singleton
싱글톤 디자인 패턴은 가장 유명하고 논란이 많은 창조 디자인 패턴 중 하나이다. 싱글톤 클래스는 애플리케이션 수명 동안 한 번만 인스턴스화하는 클래스다. 즉, 모든 리소스에 대해 하나의 개체만 공유된다. 싱글톤 방법은 스레드에 안전하며, 싱글톤 클래스 내의 공유 리소스에 액세스하는 경우에도 애플리케이션의 여러 부분에서 동시에 사용할 수 있다. 싱글톤 클래스의 사용 시기에 대한 완벽한 예는 모든 리소스가 동일한 로그 파일에 기록되고 스레드가 안전한 logger 이다. 다른 예로는 데이터베이스 연결과 공유 네트워크 자원을 들 수 있다.
또한, 애플리케이션이 서버에서 파일을 읽어야 할 때마다 싱글톤 클래스를 사용하는 것이 편리하다. 왜냐하면, 애플리케이션의 한 객체만이 서버에 저장된 파일에 액세스할 수 있기 때문이다. logger 구현 외에도, 구성 파일은 싱글톤 클래스의 사용이 효율적인 또 다른 예다.
자바에서 싱글톤은 private constructor가 있는 클래스다. 싱글톤 클래스는 클래스 자체의 인스턴스(instance)와 함께 필드를 유지한다. 객체는 인스턴스가 아직 시작되지 않은 경우 생성자를 호출하는 get 메서드를 사용하여 생성된다. 앞서 우리는 이 패턴이 인스턴스 생성을 위한 다중 구현 때문에 논란이 많다는 점을 언급하였다. 그것은 안전해야 하지만 또한 효율적이어야 한다. 예제에서 우리는 두 가지 해결책을 가지고 있다.
import java.nio.file.Files; import java.nio.file.Paths; public class LoggerSingleton{ private static Logger logger; private String logFileLocation="log.txt"; private PrintWriter pw; private FileWriter fw; private BufferedWriter bw; private Logger(){ fw = new FileWriter(logFileLocation, true); bw = new BufferedWriter(fw) this.pw = new PrintWriter(bw); } public static synchronised Logger getLogger(){ if(this.logger==null){ logger=new Logger(); } return this.logger; } public void write(String txt){ pw.println(txt); } }
왜냐하면 로그파일은 자주 접속되기 때문이다. BufferedWriter를 사용하는 PrintWriter는 파일을 여러 번 열고 닫지 않아도 된다.
두 번째 구현에는 싱글톤 클래스의 인스턴스의 정적 필드를 보유한 private 클래스가 포함된다. private 클래스는 싱글톤 클래스 내에서, 즉 get 메소드에서만 액세스할 수 있다.
public class Logger{ private static class LoggerHolder(){ public static Singleton instance=new Singleton(); } private Logger(){ // init } public static Logger getInstance(){ return LoggerHolder.instance; } }
싱글톤 클래스는 앱 내의 다른 클래스에서 사용할 수 있다.
Logger log=Logger.getInstance(); log.write("something");
찬성:
- 싱글톤 클래스는 앱의 라이프 사이클에서 한 번만 인스턴스화되며 필요한 횟수만큼 사용할 수 있다.
- 싱글톤 클래스는 공유 리소스에 대한 스레드의 안전한 액세스를 허용한다.
- 싱글톤 클래스는 확장할 수 없으며, 올바르게 구현된 경우, 즉, get 방법은 동기화되고 정적이 되어야 하며, 쓰레드에 안전하다.
- 인터페이스 테스트가 용이하기 때문에 먼저 인터페이스를 생성한 다음 싱글톤 클래스 자체를 설계하는 것이 권장된다.
반대:
- 싱글톤 클래스가 공유 리소스에 액세스하고 테스트 실행이 중요한 경우 테스트 중 문제가 발생할 수 있다.
- 또한 싱글톤 클래스는 코드에서 일부 종속성을 숨긴다. 즉, 명시적으로 생성되지 않은 종속성을 생성한다.
- factory 패턴 없이 싱글톤을 사용하는 것의 문제점은 단일 책임 원칙을 어긴다는 것이다. 왜냐하면 클래스가 자신의 라이프 사이클을 관리하고 있기 때문이다.
Builder
빌더 패턴은 또한 복잡한 물체를 점진적으로 만들 수 있는 창조 패턴이다. 필드 설정이 복잡한 작업을 요구하거나 단순히 필드 리스트가 너무 긴 경우 이 패턴을 사용하는 것이 좋다. 클래스의 모든 필드는 private inner class에 있다.
public class Example{ private String txt; private int num; public static class ExampleBuilder{ private String txt; private int num; public ExampleBuilder(int num){ this.num=num; } public ExampleBuilder withTxt(String txt){ this.txt=txt; return this; } public Example build(){ return new Example(num,txt); } } private Example(int num,String txt){ this.num=num; this.txt=txt; } }
실제의 경우, 인수의 목록이 더 길어지고 클래스에 대한 인수의 일부는 다른 입력에 기초하여 계산할 수 있다. 빌더 클래스는 클래스 자체와 동일한 필드를 가지며 홀더 클래스의 객체를 인스턴스화할 필요 없이 액세스하기 위해 정적(static)으로 선언되어야 한다(Example.java). 위에 제시된 구현은 쓰레드에 안전하며 다음과 같은 방법으로 사용할 수 있다.
Example example=new ExampleBuilder(10).withTxt("yes").build();
찬성:
- 클래스의 인수 수가 6 또는 7보다 클 경우 코드는 더 깔끔하고 재사용 가능하다.
- 객체는 필요한 모든 필드를 설정한 후 생성되며 완전히 생성된 객체만 사용할 수 있다.
- 빌더 패턴은 빌더 클래스 내부의 복잡한 계산 중 일부를 숨기고 애플리케이션 흐름에서 분리한다.
반대:
- 빌더 클래스는 원래 클래스의 모든 필드를 포함해야 하므로 클래스를 단독으로 사용하는 것에 비해 조금 더 많은 시간이 소요될 수 있다.
Observer
관찰자 설계 패턴은 특정 실체를 관찰하고 변경사항을 애플리케이션의 관련 부분에 전파하여 처리하는 행동 설계 패턴이다. 각 컨테이너는 서로 다른 설계 패턴에 대해 다른 구현을 제공할 수 있으며 관찰자 패턴은 관찰자 클래스의 변경에 영향을 받을 클래스에 대해 Observer 인터페이스를 사용하여 자바에 구현된다. 한편, 관측된 세분류는 관측 가능한 인터페이스를 구현해야 한다. Observer 인터페이스는 업데이트 방법만 가지고 있지만 단순성 때문에 사용을 권장하지 않기 때문에 Java 9에서 더 이상 사용되지 않았다. 무엇이 변했는지에 대한 세부사항을 제공하지 않으며 단순히 더 큰 물체의 변화를 찾는 것은 비용이 많이 드는 작업이 될 수 있다. 권장되는 대안은 업데이트 후 작업을 지정할 수 있는 PropertyChangeListener 인터페이스다.
반응형'JAVA' 카테고리의 다른 글
Spock Framework 의 Stub, Mock, Spy 를 간단하게 알아 보자. (0) 2020.10.15 Format number using regex in javascript (0) 2020.05.22 'Make JAR, not WAR.' - Josh Long (0) 2019.04.18 유연한 솔루션보다 단순한 솔루션을 선택하자 (0) 2019.04.01 JAVA 8 핵심만 정리했다 (0) 2019.02.22