JavaRush /Java Blog /Random-KO /안티패턴이란 무엇입니까? 예제를 살펴보겠습니다(1부)

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

Random-KO 그룹에 게시되었습니다
안티패턴이란 무엇입니까?  예제를 살펴보겠습니다(1부) - 1모두에게 좋은 하루 되세요! 얼마 전 저는 인터뷰를 했고, 안티패턴에 관한 질문을 받았습니다. 이것은 어떤 종류의 짐승이고, 그 유형과 실제 사례는 무엇입니까? 물론 나는 그 질문에 대답했지만이 문제에 대한 연구에 깊이 들어 가지 않았기 때문에 매우 피상적이었습니다. 인터뷰 이후 나는 인터넷을 뒤지기 시작했고 이 주제에 점점 더 몰입하게 되었다. 오늘 나는 가장 인기 있는 안티패턴과 그 예에 대해 간략하게 검토하고 이 문제에 대해 필요한 지식을 제공할 수 있습니다. 시작하자! 따라서 안티패턴이 무엇인지 논의하기 전에 패턴이 무엇인지 기억해 봅시다. 패턴은 애플리케이션을 설계할 때 발생하는 일반적인 문제나 상황을 해결하기 위한 반복 가능한 아키텍처 설계입니다. 그러나 오늘 우리는 그것들에 대해 이야기하는 것이 아니라 그 반대인 반패턴에 대해 이야기하고 있습니다. 안티패턴은 일반적으로 발생하는 비효율적이고 위험하며 비생산적인 문제를 해결하기 위한 일반적인 접근 방식입니다. 즉, 오류 패턴(때때로 트랩이라고도 함)입니다. 안티패턴이란 무엇입니까?  예제를 살펴보겠습니다(1부) - 2일반적으로 안티패턴은 다음 유형으로 구분됩니다.
  1. 아키텍처 안티패턴 - 시스템 구조를 설계할 때(일반적으로 아키텍트가) 발생하는 아키텍처 안티패턴입니다.
  2. 관리 안티 패턴(Management Anti Pattern) - 일반적으로 다양한 관리자(또는 관리자 그룹)가 직면하는 관리 분야의 안티패턴입니다.
  3. 개발 안티 패턴 - 안티패턴은 일반 프로그래머가 시스템을 작성할 때 발생하는 개발 문제입니다.
안티패턴의 이국성은 훨씬 더 광범위하지만 일반 개발자에게는 이것이 압도적일 것이기 때문에 오늘은 고려하지 않을 것입니다. 우선, 경영 분야의 안티패턴의 예를 들어보겠습니다.

1. 분석 마비

분석 마비는 전형적인 조직의 반패턴으로 간주됩니다. 계획을 세울 때 상황을 과도하게 분석하여 결정이나 조치가 취해지지 않아 본질적으로 개발이 마비됩니다. 이는 분석 기간의 완벽함과 완전한 완료를 목표로 하는 경우에 자주 발생합니다. 이 안티패턴은 원을 그리며(일종의 폐쇄 루프), 세부 모델을 수정 및 생성하는 것이 특징이며, 이는 결국 작업 흐름을 방해합니다. 예를 들어 다음과 같은 것을 예측하려고 합니다. 사용자가 갑자기 직원 이름의 네 번째와 다섯 번째 글자를 기반으로 직원 목록을 만들고 싶어하는 경우에는 직원이 근무 시간 사이에 가장 많은 근무 시간을 바친 프로젝트를 목록에 포함시킵니다. 새해와 지난 4년의 3월 8일은요? 본질적으로 이것은 분석의 과잉입니다. 실생활의 좋은 예는 분석 마비로 인해 Kodak이 어떻게 파산했는지를 들 수 있습니다 . 다음은 분석 마비를 방지하기 위한 몇 가지 작은 팁입니다.
  1. 장기 목표를 의사 결정의 신호로 정의하여 모든 결정이 목표에 더 가까워지고 시간을 표시하도록 강요하지 않도록 해야 합니다.
  2. 사소한 일에 집중하지 마세요. (사소한 뉘앙스에 인생의 마지막인 것처럼 결정을 내리는 이유는 무엇입니까?)
  3. 결정을 내리는 기한을 정하십시오.
  4. 어떤 일을 완벽하게 하려고 하지 마세요. 아주 잘하는 것이 더 좋습니다.
우리는 지금 너무 깊이 들어가지 않고 다른 관리 반패턴을 고려하지 않을 것입니다. 따라서 서문 없이 몇 가지 아키텍처 안티패턴으로 넘어가겠습니다. 왜냐하면 이 문서는 관리자가 아닌 미래의 개발자가 읽을 가능성이 높기 때문입니다.

2.신의 대상

Divine Object는 너무 많은 이질적인 기능이 과도하게 집중되어 많은 양의 다양한 데이터(애플리케이션이 회전하는 객체)를 저장하는 것을 설명하는 안티 패턴입니다. 작은 예를 들어보겠습니다:
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비즈니스 로직이 포함된 Facade 메소드도 볼 수 있습니다. 그러한 신의 대상은 적절하게 지탱하기에는 거대하고 투박해집니다. 우리는 코드의 모든 부분에서 이를 수정해야 합니다. 시스템의 많은 노드가 이에 의존하고 밀접하게 연결되어 있습니다. 이러한 코드를 유지하는 것은 점점 더 어려워지고 있습니다. 이러한 경우에는 별도의 클래스로 나누어야 하며 각 클래스는 하나의 목적(목표)만 갖습니다. 이 예에서는 이를 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부) - 3이 템플릿의 단점을 살펴보겠습니다.
  1. 글로벌 상태. 클래스의 인스턴스에 액세스할 때 해당 클래스의 현재 상태, 누가 언제 변경했는지 알 수 없으며 해당 상태가 우리가 기대하는 것과 다를 수도 있습니다. 즉, 싱글톤 작업의 정확성은 호출 순서에 따라 달라지며, 이로 인해 하위 시스템이 서로 의존하게 되고 결과적으로 개발 복잡성이 심각하게 증가합니다.

  2. 싱글톤은 SOLID 원칙 중 하나인 단일 책임 원칙을 위반합니다. 싱글톤 클래스는 즉각적인 책임을 수행하는 것 외에도 인스턴스 수를 제어합니다.

  3. 싱글톤에 대한 일반 클래스의 종속성은 클래스 인터페이스에 표시되지 않습니다. 일반적으로 싱글톤 인스턴스는 메소드 매개변수로 전달되지 않고 를 통해 직접 획득되므로 getInstance()싱글톤에 대한 클래스의 종속성을 식별하기 위해 각 메소드의 구현을 자세히 조사해야 합니다. 단순히 객체의 공개 계약을 보는 것만으로는 충분하지 않습니다. .

    싱글톤이 있으면 일반적으로 애플리케이션의 테스트 용이성과 특히 싱글톤을 사용하는 클래스가 감소합니다. 첫째, 싱글톤 대신 Mock 객체를 배치할 수 없으며, 둘째, 싱글톤에 상태 변경을 위한 인터페이스가 있는 경우 테스트는 서로 의존하게 됩니다.

    즉, 싱글톤은 연결성을 증가시키며 위의 모든 것은 연결성 증가의 결과에 지나지 않습니다.

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

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

    이제 이 주제에 대한 많은 holivar가 있으므로 싱글톤이 패턴인지 안티패턴인지 결정하는 것은 사용자에게 달려 있습니다.

    그리고 우리는 그것에 대해 더 이상 생각하지 않고 오늘의 마지막 디자인 패턴인 폴터가이스트로 넘어갈 것입니다.

4. 폴터가이스트

Poltergeist 는 다른 클래스의 메서드를 호출하거나 단순히 불필요한 추상화 계층을 추가하는 데 사용되는 유용하지 않은 클래스 안티패턴입니다. 안티패턴은 상태가 없는 수명이 짧은 개체의 형태로 나타납니다. 이러한 개체는 내구성이 더 뛰어난 다른 개체를 초기화하는 데 자주 사용됩니다.
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. 보트 앵커

안티패턴의 맥락에서 보트 앵커는 일부 최적화 또는 리팩토링 후에 남은 시스템의 사용되지 않는 부분을 저장하는 것을 의미합니다. 또한 코드의 일부 부분은 나중에 다시 사용해야 할 경우를 대비해 "미래를 위해" 남겨 둘 수 있습니다. 이는 본질적으로 코드를 쓰레기통으로 만듭니다. 안티패턴이란 무엇입니까?  예제를 살펴보겠습니다(1부) - 4예:
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());
}
데이터베이스에 있는 사용자의 데이터와 업데이트를 위해 온 사람의 데이터를 병합하기 위해 별도의 방법을 사용하는 업데이트 방법이 있습니다(업데이트를 위해 온 사람의 필드가 비어 있는 경우 이전 데이터로 기록됩니다) 데이터베이스에서). 예를 들어, 레코드를 이전 레코드와 병합하지 말고, 빈 필드가 있더라도 덮어써야 한다는 요구 사항이 있었습니다.
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개의 복사본으로 채워집니다. 그러한 객체를 가져오면 풀에서 제거되고(없으면 생성되어 즉시 제공됩니다) 마지막에는 객체를 다시 넣는 방법이 있습니다. 객체는 Resource다음과 같습니다.
public class Resource {
   private Map<String, String> patterns;
   public Resource() {
       patterns = new HashMap<>();
       patterns.put("заместитель", "https://studfile.net/preview/3676297/page:3/");
       patterns.put("мост", "https://studfile.net/preview/3676297/page:4/");
       patterns.put("фасад", "https://studfile.net/preview/3676297/page:5/");
       patterns.put("строитель", "https://studfile.net/preview/3676297/page:6/#16");
   }
   public Map<String, String> getPatterns() {
       return patterns;
   }
   public void setPatterns(Map<String, String> patterns) {
       this.patterns = patterns;
   }
}
여기에 패턴 이름을 키로, 패턴에 대한 링크를 값으로 포함하는 맵과 맵에 액세스하기 위한 메서드가 포함된 작은 개체가 있습니다. 한번 보자 main:
class SomeMain {
   public static void main(String[] args) {
       ReusablePool pool = ReusablePool.getInstance();

       Resource firstResource = pool.acquireResource();
       Map<String, String> firstPatterns = firstResource.getPatterns();
       // ......Howим-то образом используем нашу мапу.....
       pool.releaseResource(firstResource);

       Resource secondResource = pool.acquireResource();
       Map<String, String> secondPatterns = firstResource.getPatterns();
       // ......Howим-то образом используем нашу мапу.....
       pool.releaseResource(secondResource);

       Resource thirdResource = pool.acquireResource();
       Map<String, String> thirdPatterns = firstResource.getPatterns();
       // ......Howим-то образом используем нашу мапу.....
       pool.releaseResource(thirdResource);
   }
}
여기서도 모든 것이 명확합니다. 풀 개체를 가져와서 리소스가 포함된 개체를 꺼내고, 맵을 가져와서 작업을 수행하고 추가 재사용을 위해 모두 다시 풀에 넣습니다. 짜잔: 여기에 개체 풀 패턴이 있습니다. 하지만 우리는 안티패턴에 대해 이야기하고 있었습니다. 그렇죠? 이 사례를 살펴보겠습니다 main.
Resource fourthResource = pool.acquireResource();
   Map<String, String> fourthPatterns = firstResource.getPatterns();
// ......Howим-то образом используем нашу мапу.....
fourthPatterns.clear();
firstPatterns.put("first","blablabla");
firstPatterns.put("second","blablabla");
firstPatterns.put("third","blablabla");
firstPatterns.put("fourth","blablabla");
pool.releaseResource(fourthResource);
여기서도 리소스 개체를 가져오고 패턴이 있는 맵을 가져와서 작업을 수행하지만 개체 풀에 다시 저장하기 전에 맵을 정리하고 이 리소스 개체를 재사용하기에 적합하지 않게 만드는 이해할 수 없는 데이터로 채웁니다. 개체 풀의 주요 차이점 중 하나는 개체가 반환된 후 추가 재사용에 적합한 상태로 반환되어야 한다는 것입니다. 개체가 풀로 반환된 후 올바르지 않거나 정의되지 않은 상태인 경우 이 구성을 개체 cesspool이라고 합니다. 재사용에 적합하지 않은 물건을 보관하는 것이 합리적입니까? 이 상황에서는 생성자에서 내부 맵을 불변으로 만들 수 있습니다.
public Resource() {
   patterns = new HashMap<>();
   patterns.put("заместитель", "https://studfile.net/preview/3676297/page:3/");
   patterns.put("мост", "https://studfile.net/preview/3676297/page:4/");
   patterns.put("фасад", "https://studfile.net/preview/3676297/page:5/");
   patterns.put("строитель", "https://studfile.net/preview/3676297/page:6/#16");
   patterns = Collections.unmodifiableMap(patterns);
}
(컨텐츠를 변경하려는 시도와 욕구는 UnsupportedOperationException과 함께 발생합니다). 안티패턴은 급격한 시간 부족, 부주의, 경험 부족 또는 관리자의 추방으로 인해 개발자가 자주 빠지는 함정입니다. 평소 시간이 부족하고 서두르면 향후 신청에 큰 문제가 발생할 수 있으므로 이러한 실수를 미리 알고 피해야 합니다. 안티패턴이란 무엇입니까?  예제를 살펴보겠습니다(1부) - 6이것으로 기사의 첫 번째 부분이 끝났습니다. 계속하려면 .
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION