CodeGym /Java Blog /무작위의 /안티패턴이란 무엇입니까? 몇 가지 예를 살펴보겠습니다(1부).
John Squirrels
레벨 41
San Francisco

안티패턴이란 무엇입니까? 몇 가지 예를 살펴보겠습니다(1부).

무작위의 그룹에 게시되었습니다
모두에게 좋은 하루! 얼마 전 취업 면접을 봤는데 안티패턴이 무엇인지, 어떤 유형이 있는지, 어떤 실용적인 예가 있는지 등 몇 가지 질문을 받았습니다. 물론 나는 질문에 대답했지만 이전에 이 주제에 대해 깊이 파고들지 않았기 때문에 매우 피상적으로 대답했습니다. 인터뷰 후 인터넷을 샅샅이 뒤지기 시작했고 점점 더 주제에 몰두했습니다. 안티패턴이란 무엇입니까?  몇 가지 예를 살펴보겠습니다(1부) - 1 오늘 저는 가장 인기 있는 안티 패턴에 대한 간략한 개요를 제공하고 몇 가지 예를 검토하고자 합니다. 이 글을 읽으면 이 분야에서 필요한 지식을 얻을 수 있기를 바랍니다. 시작하자! 안티패턴이 무엇인지 논의하기 전에 디자인 패턴이 무엇인지 생각해 봅시다. 디자인 패턴응용 프로그램을 설계할 때 발생하는 일반적인 문제 또는 상황에 대한 반복 가능한 아키텍처 솔루션입니다. 그러나 오늘 우리는 그들에 대해 이야기하는 것이 아니라 그 반대인 안티 패턴에 대해 이야기하고 있습니다. 반패턴은 일반적인 문제를 해결하기 위해 광범위하지만 비효율적이고 위험하며 비생산적인 접근 방식입니다. 즉, 이것은 실수의 패턴입니다(함정이라고도 함). 일반적으로 안티패턴은 다음 유형으로 나뉩니다.
  1. Architectural anti-patterns — 이러한 안티패턴은 시스템 구조가 설계될 때 발생합니다(일반적으로 설계자에 의해).
  2. 관리/조직적 반패턴 — 일반적으로 다양한 관리자(또는 관리자 그룹)가 접하는 프로젝트 관리의 반패턴입니다.
  3. 개발 안티패턴 — 이러한 안티패턴은 시스템이 일반 프로그래머에 의해 구현될 때 발생합니다.
안티 패턴의 전체 범위는 훨씬 더 이국적이지만 오늘날 모든 안티 패턴을 고려하지는 않습니다. 일반 개발자에게는 너무 많은 것입니다. 우선 관리 안티 패턴을 예로 들어 보겠습니다.

1. 분석적 마비

분석 마비고전적인 관리 안티 패턴으로 간주됩니다. 계획 중에 상황을 과도하게 분석하여 어떤 결정이나 조치도 취하지 않고 본질적으로 개발 프로세스를 마비시킵니다. 이것은 목표가 완벽을 달성하고 분석 기간 동안 절대적으로 모든 것을 고려하는 것일 때 종종 발생합니다. 이 반패턴은 원을 그리며(평범한 폐쇄 루프) 걷고 세부 모델을 수정 및 생성하여 워크플로를 방해하는 것이 특징입니다. 예를 들어, 한 수준에서 사물을 예측하려고 합니다. 그러나 사용자가 갑자기 직원 이름의 네 번째와 다섯 번째 글자를 기반으로 직원 목록을 만들고자 하는 경우에는 가장 많은 시간을 보낸 프로젝트 목록을 포함하여 어떻게 해야 합니까? 지난 4년 동안 설날과 세계 여성의 날 사이에? 본질적으로 그것은 ' 너무 많은 분석입니다. 다음은 분석 마비와 싸우기 위한 몇 가지 요령입니다.
  1. 장기적인 목표를 의사 결정을 위한 등불로 정의해야 각 결정이 정체되지 않고 목표에 더 가까이 다가갈 수 있습니다.
  2. 사소한 일에 집중하지 말라(왜 하찮은 일을 인생에서 가장 중요한 결정인 것처럼 결정하지?)
  3. 결정 기한을 정하십시오.
  4. 작업을 완벽하게 완료하려고 하지 마십시오. 아주 잘 수행하는 것이 좋습니다.
여기에서 너무 깊이 들어갈 필요가 없으므로 다른 관리적 반패턴을 고려하지 않을 것입니다. 따라서 소개 없이 몇 가지 아키텍처 안티 패턴으로 넘어갈 것입니다. 이 기사는 관리자가 아닌 미래의 개발자가 읽을 가능성이 높기 때문입니다.

2. 신의 대상

God 객체는 모든 종류의 기능과 많은 양의 이질적인 데이터(응용 프로그램이 중심이 되는 객체)의 과도한 집중을 설명하는 안티 패턴입니다. 작은 예를 들어 보겠습니다.

public class SomeUserGodObject {
   private static final String FIND_ALL_USERS_EN = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users;
   private static final String FIND_BY_ID = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users WHERE id = ?";
   private static final String FIND_ALL_CUSTOMERS = "SELECT id, u.email, u.phone, u.first_name_en, u.middle_name_en, u.last_name_en, u.created_date" +
           "  WHERE u.id IN (SELECT up.user_id FROM user_permissions up WHERE up.permission_id = ?)";
   private static final String FIND_BY_EMAIL = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_dateFROM users WHERE email = ?";
   private static final String LIMIT_OFFSET = " LIMIT ? OFFSET ?";
   private static final String ORDER = " ORDER BY ISNULL(last_name_en), last_name_en, ISNULL(first_name_en), first_name_en, ISNULL(last_name_ru), " +
           "last_name_ru, ISNULL(first_name_ru), first_name_ru";
   private static final String CREATE_USER_EN = "INSERT INTO users(id, phone, email, first_name_en, middle_name_en, last_name_en, created_date) " +
           "VALUES (?, ?, ?, ?, ?, ?, ?)";
   private static final String FIND_ID_BY_LANG_CODE = "SELECT id FROM languages WHERE lang_code = ?";
                                  ........
   private final JdbcTemplate jdbcTemplate;
   private Map<String, String> firstName;
   private Map<String, String> middleName;
   private Map<String, String> lastName;
   private List<Long> permission;
                                   ........
   @Override
   public List<User> findAllEnCustomers(Long permissionId) {
       return jdbcTemplate.query( FIND_ALL_CUSTOMERS + ORDER, userRowMapper(), permissionId);
   }
   @Override
   public List<User> findAllEn() {
       return jdbcTemplate.query(FIND_ALL_USERS_EN + ORDER, userRowMapper());
   }
   @Override
   public Optional<List<User>> findAllEnByEmail(String email) {
       var query = FIND_ALL_USERS_EN + FIND_BY_EMAIL + ORDER;
       return Optional.ofNullable(jdbcTemplate.query(query, userRowMapper(), email));
   }
                              .............
   private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
       switch (type) {
           case USERS:
               return findAllEnUsers(permissionId);
           case CUSTOMERS:
               return findAllEnCustomers(permissionId);
           default:
               return findAllEn();
       }
   }
                              ..............…

   private RowMapper<User> userRowMapperEn() {
       return (rs, rowNum) ->
               User.builder()
                       .id(rs.getLong("id"))
                       .email(rs.getString("email"))
                       .accessFailed(rs.getInt("access_counter"))
                       .createdDate(rs.getObject("created_date", LocalDateTime.class))
                       .firstName(rs.getString("first_name_en"))
                       .middleName(rs.getString("middle_name_en"))
                       .lastName(rs.getString("last_name_en"))
                       .phone(rs.getString("phone"))
                       .build();
   }
}
여기서 우리는 모든 것을 수행하는 거대한 클래스를 봅니다. 여기에는 데이터베이스 쿼리와 일부 데이터가 포함됩니다. 또한 비즈니스 로직을 포함하는 findAllWithoutPageEn 파사드 메서드도 볼 수 있습니다. 그러한 신의 대상은 적절하게 유지하기에는 거대하고 어색해집니다. 우리는 코드의 모든 부분에서 그것을 다루어야 합니다. 많은 시스템 구성 요소가 이에 의존하고 밀접하게 결합되어 있습니다. 그러한 코드를 유지하기가 점점 더 어려워집니다. 이러한 경우 코드는 별도의 클래스로 분할되어야 하며 각 클래스는 하나의 목적만 갖습니다. 이 예에서는 God 개체를 Dao 클래스로 분할할 수 있습니다.

public class UserDaoImpl {
   private static final String FIND_ALL_USERS_EN = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users;
   private static final String FIND_BY_ID = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users WHERE id = ?";
  
                                   ........
   private final JdbcTemplate jdbcTemplate;
                                                        
                                   ........
   @Override
   public List<User> findAllEnCustomers(Long permissionId) {
       return jdbcTemplate.query(FIND_ALL_CUSTOMERS + ORDER, userRowMapper(), permissionId);
   }
   @Override
   public List<User> findAllEn() {
       return jdbcTemplate.query(FIND_ALL_USERS_EN + ORDER, userRowMapper());
   }
  
                               ........
}
데이터 및 데이터에 액세스하기 위한 메서드를 포함하는 클래스:

public class UserInfo {
   private Map<String, String> firstName;
                     …..
   public Map<String, String> getFirstName() {
       return firstName;
   }
   public void setFirstName(Map<String, String> firstName) {
       this.firstName = firstName;
   }
                    ....
그리고 비즈니스 로직이 있는 메서드를 서비스로 옮기는 것이 더 적절할 것입니다.

private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
   switch (type) {
       case USERS:
           return findAllEnUsers(permissionId);
       case CUSTOMERS:
           return findAllEnCustomers(permissionId);
       default:
           return findAllEn();
   }
}

3. 싱글턴

싱글톤은 가장 단순한 패턴입니다. 단일 스레드 응용 프로그램에서 클래스의 단일 인스턴스가 있는지 확인하고 이 개체에 대한 전역 액세스 지점을 제공합니다. 하지만 패턴인가, 안티패턴인가? 이 패턴의 단점을 살펴보겠습니다.
  1. 전역 상태 클래스의 인스턴스에 액세스할 때 이 클래스의 현재 상태를 알 수 없습니다. 누가 언제 변경했는지 알 수 없습니다. 상태는 우리가 기대하는 것과 다를 수 있습니다. 즉, 싱글톤 작업의 정확성은 액세스 순서에 따라 달라집니다. 이는 하위 시스템이 서로 의존적이며 결과적으로 설계가 훨씬 더 복잡해진다는 것을 의미합니다.

  2. 싱글톤은 단일 책임 원칙인 SOLID 원칙을 위반합니다. 싱글톤 클래스는 직접적인 의무 외에도 인스턴스 수를 제어합니다.

  3. 싱글톤에 대한 일반 클래스의 종속성은 클래스의 인터페이스에서 볼 수 없습니다. 싱글톤 인스턴스는 일반적으로 메서드 인수로 전달되지 않고 대신 getInstance()를 통해 직접 가져오기 때문에 싱글톤에 대한 클래스의 종속성을 식별하기 위해 각 메서드의 구현에 들어가야 합니다. 계약이 충분하지 않습니다.

    싱글톤의 존재는 애플리케이션 전체와 특히 싱글톤을 사용하는 클래스의 테스트 가능성을 감소시킵니다. 우선, 싱글톤을 모의 객체로 바꿀 수 없습니다. 둘째, 싱글톤에 상태 변경을 위한 인터페이스가 있는 경우 테스트는 서로 의존하게 됩니다.

    즉, 싱글톤은 결합도를 증가시키고, 위에서 언급한 모든 것은 결합도 증가의 결과에 지나지 않습니다.

    그리고 생각해보면 싱글톤 사용을 피할 수 있습니다. 예를 들어 객체의 인스턴스 수를 제어하기 위해 다양한 종류의 팩토리를 사용하는 것이 가능합니다(실제로 필요합니다).

    가장 큰 위험은 싱글톤을 기반으로 전체 애플리케이션 아키텍처를 구축하려는 시도에 있습니다. 이 접근 방식에 대한 수많은 훌륭한 대안이 있습니다. 가장 중요한 예는 Spring, 즉 IoC 컨테이너입니다. Spring은 실제로 "스테로이드 공장"이기 때문에 서비스 생성을 제어하는 ​​문제에 대한 자연스러운 솔루션입니다.

    끝없이 화해할 수 없는 많은 논쟁이 현재 이 주제에 대해 맹위를 떨치고 있습니다. 싱글톤이 패턴인지 반패턴인지는 사용자가 결정합니다.

    우리는 그것에 머물지 않을 것입니다. 대신 오늘의 마지막 디자인 패턴인 폴터가이스트로 넘어갈 것입니다.

4. 폴터가이스트

폴터가이스트 는 다른 클래스의 메서드를 호출하거나 단순히 불필요한 추상화 계층을 추가하는 데 사용되는 무의미한 클래스와 관련된 반패턴입니다. 이 반패턴은 상태가 없는 수명이 짧은 개체로 나타납니다. 이러한 개체는 종종 다른 보다 영구적인 개체를 초기화하는 데 사용됩니다.

public class UserManager {
   private UserService service;
   public UserManager(UserService userService) {
       service = userService;
   }
   User createUser(User user) {
       return service.create(user);
   }
   Long findAllUsers(){
       return service.findAll().size();
   }
   String findEmailById(Long id) {
       return service.findById(id).getEmail();}
   User findUserByEmail(String email) {
       return service.findByEmail(email);
   }
   User deleteUserById(Long id) {
       return service.delete(id);
   }
}
중개자에 불과하고 작업을 다른 사람에게 위임하는 개체가 필요한 이유는 무엇입니까? 우리는 그것을 제거하고 수명이 긴 개체에 있었던 작은 기능을 이전합니다. 다음으로 우리는 (일반 개발자로서) 가장 관심 있는 패턴, 즉 개발 방지 패턴 으로 이동합니다 .

5. 하드 코딩

그래서 우리는 하드 코딩이라는 끔찍한 단어에 도달했습니다. 이 안티 패턴의 본질은 코드가 특정 하드웨어 구성 및/또는 시스템 환경에 강력하게 연결되어 있다는 것입니다. 이는 코드를 다른 구성으로 포팅하는 것을 크게 복잡하게 만듭니다. 이 안티패턴은 매직 넘버와 밀접한 관련이 있습니다(이러한 안티패턴은 종종 서로 얽혀 있습니다). 예:

public Connection buildConnection() throws Exception {
   Class.forName("com.mysql.cj.jdbc.Driver");
   connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8&characterSetResults=UTF-8&serverTimezone=UTC", "user01", "12345qwert");
   return connection;
}
아프지 않니? 여기에서 연결 설정을 하드 코딩합니다. 결과적으로 코드는 MySQL에서만 올바르게 작동합니다. 데이터베이스를 변경하려면 코드를 자세히 살펴보고 모든 것을 수동으로 변경해야 합니다. 좋은 해결책은 구성을 별도의 파일에 넣는 것입니다.

spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
또 다른 옵션은 상수를 사용하는 것입니다.

6. 보트 앵커

안티 패턴의 맥락에서 보트 앵커는 일부 최적화 또는 리팩토링을 수행한 후 더 이상 사용되지 않는 시스템 부분을 유지하는 것을 의미합니다. 또한 코드의 일부는 갑자기 필요할 경우를 대비하여 "나중에 사용하기 위해" 보관할 수 있습니다. 본질적으로 이것은 코드를 쓰레기통으로 바꿉니다. 예:

public User update(Long id, User request) {
   User user = mergeUser(findById(id), request);
   return userDAO.update(user);
}
private User mergeUser(User findUser, User requestUser) {
   return new User(
           findUser.getId(),
           requestUser.getEmail() != null ? requestUser.getEmail() : findUser.getEmail(),
           requestUser.getFirstName() != null ? requestUser.getFirstName() : findUser.getFirstNameRu(),
           requestUser.getMiddleName() != null ? requestUser.getMiddleName() : findUser.getMiddleNameRu(),
           requestUser.getLastName() != null ? requestUser.getLastName() : findUser.getLastNameEn(),
           requestUser.getPhone() != null ? requestUser.getPhone() : findUser.getPhone());
}
별도의 메서드를 사용하여 데이터베이스의 사용자 데이터를 메서드에 전달된 사용자 데이터와 병합하는 업데이트 메서드가 있습니다(업데이트 메서드에 전달된 사용자에 null 필드가 있는 경우 데이터베이스에서 이전 필드 값을 가져옴). . 그런 다음 레코드가 이전 레코드와 병합되어서는 안 되며 대신 null 필드가 있더라도 이전 레코드를 덮어쓰는 데 사용된다는 새로운 요구 사항이 있다고 가정합니다.

public User update(Long id, User request) {
   return userDAO.update(user);
}
이는 mergeUser가 더 이상 사용되지 않는다는 것을 의미하지만 삭제하는 것이 유감입니다. 이 방법(또는 이 방법의 아이디어)이 언젠가 유용하게 사용될 수 있다면 어떨까요? 이러한 코드는 시스템을 복잡하게 만들고 혼란을 야기할 뿐 실질적인 가치가 없습니다. "죽은 조각"이 있는 코드는 다른 프로젝트로 떠날 때 동료에게 전달하기 어렵다는 점을 잊지 말아야 합니다. 보트 앵커를 처리하는 가장 좋은 방법은 코드를 리팩터링하는 것입니다. 즉, 코드 섹션을 삭제하는 것입니다. 또한 개발 일정을 준비할 때 이러한 앵커를 고려해야 합니다(정리를 위한 시간 할당).

7. 개체 오물 풀

이 안티 패턴을 설명하려면 먼저 개체 풀 패턴 에 익숙해져야 합니다 . 객체 (리소스 풀)은 초기화되고 사용 준비가 된 객체 집합인 생성 디자인 패턴 입니다. 응용 프로그램에 개체가 필요한 경우 개체를 다시 만들지 않고 이 풀에서 가져옵니다. 객체가 더 이상 필요하지 않으면 소멸되지 않습니다. 대신 풀로 반환됩니다. 이 패턴은 일반적으로 데이터베이스에 연결할 때와 같이 필요할 때마다 생성하는 데 시간이 많이 걸리는 무거운 개체에 사용됩니다. 작고 간단한 예를 살펴보겠습니다. 다음은 이 패턴을 나타내는 클래스입니다.

class ReusablePool {
   private static ReusablePool pool;
   private List<Resource> list = new LinkedList<>();
   private ReusablePool() {
       for (int i = 0; i < 3; i++)
           list.add(new Resource());
   }
   public static ReusablePool getInstance() {
       if (pool == null) {
           pool = new ReusablePool();
       }
       return pool;
   }
   public Resource acquireResource() {
       if (list.size() == 0) {
           return new Resource();
       } else {
           Resource r = list.get(0);
           list.remove(r);
           return r;
       }
   }
   public void releaseResource(Resource r) {
       list.add(r);
   }
}
이 클래스는 위의 싱글톤 패턴/안티 패턴 의 형태로 제공됩니다 . 즉, 이 유형의 개체는 하나만 있을 수 있습니다. 특정 개체를 사용합니다 Resource. 기본적으로 생성자는 4개의 인스턴스로 풀을 채웁니다. 개체를 가져오면 풀에서 제거됩니다(사용 가능한 개체가 없으면 개체가 생성되고 즉시 반환됨). 그리고 마지막에 객체를 다시 넣는 방법이 있습니다. 자원 객체는 다음과 같습니다.

public class Resource {
   private Map<String, String> patterns;
   public Resource() {
       patterns = new HashMap<>();
       patterns.put("proxy", "https://en.wikipedia.org/wiki/Proxy_pattern");
       patterns.put("bridge", "https://en.wikipedia.org/wiki/Bridge_pattern");
       patterns.put("facade", "https://en.wikipedia.org/wiki/Facade_pattern");
       patterns.put("builder", "https://en.wikipedia.org/wiki/Builder_pattern");
   }
   public Map<String, String> getPatterns() {
       return patterns;
   }
   public void setPatterns(Map<String, String> patterns) {
       this.patterns = patterns;
   }
}
여기에는 디자인 패턴 이름을 키로, 해당 Wikipedia 링크를 값으로 포함하는 지도와 지도에 액세스하는 메서드가 포함된 작은 개체가 있습니다. 메인을 살펴보자:

class SomeMain {
   public static void main(String[] args) {
       ReusablePool pool = ReusablePool.getInstance();

       Resource firstResource = pool.acquireResource();
       Map<String, String> firstPatterns = firstResource.getPatterns();
       // use our map somehow...
       pool.releaseResource(firstResource);

       Resource secondResource = pool.acquireResource();
       Map<String, String> secondPatterns = firstResource.getPatterns();
       // use our map somehow...
       pool.releaseResource(secondResource);

       Resource thirdResource = pool.acquireResource();
       Map<String, String> thirdPatterns = firstResource.getPatterns();
       // use our map somehow...
       pool.releaseResource(thirdResource);
   }
}
여기에 있는 모든 것은 충분히 명확합니다. 풀 개체를 가져오고, 풀에서 리소스가 있는 개체를 가져오고, 리소스 개체에서 지도를 가져오고, 지도를 사용하여 작업을 수행하고, 추가 재사용을 위해 이 모든 것을 풀에 배치합니다. Voila, 이것은 개체 풀 디자인 패턴입니다. 하지만 우리는 반 패턴에 대해 이야기하고 있었습니다. 맞습니까? main 메서드에서 다음과 같은 경우를 살펴보겠습니다.

Resource fourthResource = pool.acquireResource();
   Map<String, String> fourthPatterns = firstResource.getPatterns();
// use our map somehow...
fourthPatterns.clear();
firstPatterns.put("first","blablabla");
firstPatterns.put("second","blablabla");
firstPatterns.put("third","blablabla");
firstPatterns.put("fourth","blablabla");
pool.releaseResource(fourthResource);
여기에서 다시 Resource 객체를 얻고 패턴 맵을 얻고 맵으로 작업을 수행합니다. 그러나 지도를 개체 풀에 다시 저장하기 전에 지도가 지워지고 손상된 데이터로 채워져 리소스 개체가 재사용에 적합하지 않게 됩니다. 개체 풀의 주요 세부 사항 중 하나는 개체가 반환될 때 추가 재사용에 적합한 상태로 복원되어야 한다는 것입니다. 풀로 반환된 객체가 부정확하거나 정의되지 않은 상태로 남아 있으면 우리의 설계를 객체 cesspool이라고 합니다. 재사용에 적합하지 않은 개체를 저장하는 것이 의미가 있습니까? 이 상황에서 생성자에서 내부 맵을 변경할 수 없도록 만들 수 있습니다.

public Resource() {
   patterns = new HashMap<>();
   patterns.put("proxy", "https://en.wikipedia.org/wiki/Proxy_pattern");
   patterns.put("bridge", "https://en.wikipedia.org/wiki/Bridge_pattern");
   patterns.put("facade", "https://en.wikipedia.org/wiki/Facade_pattern");
   patterns.put("builder", "https://en.wikipedia.org/wiki/Builder_pattern");
   patterns = Collections.unmodifiableMap(patterns);
}
지도의 콘텐츠를 변경하려는 시도와 욕구는 그들이 생성하는 UnsupportedOperationException 덕분에 사라질 것입니다. 안티 패턴은 극심한 시간 부족, 부주의, 경험 부족 또는 프로젝트 관리자의 압력으로 인해 개발자가 자주 직면하는 함정입니다. 흔하게 발생하는 러싱은 향후 애플리케이션에 큰 문제로 이어질 수 있으므로 이러한 오류를 미리 파악하고 예방해야 합니다. 이것으로 기사의 첫 번째 부분을 마칩니다. 계속하려면...
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION