군만두의 IT 공부 일지

[스터디6] 04. 자바가 확장한 객체 지향 본문

프로그래밍/객체지향

[스터디6] 04. 자바가 확장한 객체 지향

mandus 2025. 3. 22. 11:31

목차

     

    이번에는 책에서 읽은 내용을 현재 진행 중인 MSA(Microservice Architecture) 기반 물류 시스템의 '주문 서비스'와 연결하여 정리했습니다.

    04. 자바가 확장한 객체 지향

    1. abstract 키워드 - 추상 메서드와 추상 클래스

    • 추상 메서드(Abstract Method): 선언부는 있는데 구현부가 없는 메서드
    • 추상 메서드를 하나라도 갖고 있는 클래스는 반드시 추상 클래스(Abstract Class)로 선언해야 한다.
    • 추상 메서드 없이도 추상 클래스를 선언할 수는 있다.
    - 추상 클래스는 인스턴스, 즉 객체를 만들 수 없다. 즉, new를 사용할 수 없다.
    - 추상 메서드는 하위 클래스에게 메서드의 구현을 강제한다. 오버라이딩 강제.
    - 추상 메서드를 포함하는 클래스는 반드시 추상 클래스여야 한다.

     

    추상 클래스는 직접 인스턴스화할 수 없고, 상속을 통해서만 사용할 수 있는 클래스다. 이 프로젝트에서는 Timestamped를 추상 클래스라고 할 수 있다.

    @Getter
    @Setter
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public abstract class Timestamped {
    
        @CreatedDate
        @Column(name = "created_at", updatable = false)
        @Temporal(TemporalType.TIMESTAMP)
        private LocalDateTime createdAt;
    
        @CreatedBy
        @Column(name = "created_by", length = 50, updatable = false)
        private String createdBy;
    
        // ... 기타 필드 및 메서드
        
        public void delete(String deletedBy) {
            this.deletedBy = deletedBy;
            this.deletedAt = LocalDateTime.now();
        }
    }

    위 추상 클래스는 엔티티의 생성/수정/삭제 시간과 사용자 정보를 자동으로 관리하는 공통 기능을 제공한다. Order와 OrderProduct 클래스는 이 추상 클래스를 상속받아 사용한다.

    @Entity
    @Table(name = "p_order")
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class Order extends Timestamped {
        // ... 필드 및 메서드
    }

    추상 클래스를 사용함으로써 코드 중복을 방지하고, 공통된 기능을 한 곳에서 관리할 수 있다.

    2. 생성자

    • 클래스의 인스턴스, 즉 객체를 만들 때마다 new 키워드를 사용한다.
      • 개발자가 아무런 생성자도 만들지 않으면 자바는 인자가 없는 기본 생성자를 자동으로 만들어준다.
      • 인자가 있는 생성자를 하나라도 만든다면 자바는 기본 생성자를 만들어 주지 않는다.
    • 생성자는 개발자가 필요한 만큼 오버로딩해서 만들 수 있다.
    • 생성자로 줄여 부르지만 정확히 표현하면 객체 생성자 메서드다.

    자바의 생성자는 객체가 생성될 때 초기화 작업을 담당한다. 이 프로젝트에는 다양한 생성자 패턴을 적용했다.

     

    1. Lombok 어노테이션을 통한 생성자 자동 생성

    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class Order extends Timestamped {
        // ... 필드
    }

    2. 이벤트 클래스에서 생성자 패턴 사용

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class OrderCreatedEvent {
        private UUID orderId;
        private UUID deliveryId;
        // ... 기타 필드
    }

    3. Timestamped 클래스의 명시적 생성자

    public Timestamped() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            createdBy = authentication.getName();
        }
    }

    3. 클래스 생성 시의 실행 블록, static 블록

    • 객체 생성자가 있지만, 클래스 생성자는 존재하지 않는다.
    • 클래스가 스태틱 영역에 배치될 때 실행되는 코드 블록(static 블록)이 있다.
    • static 블록에서 사용할 수 있는 속성과 메서드는 static 멤버 뿐이다.
    클래스가 처음 사용될 경우
    - 클래스의 정적 속성을 사용할 때
    - 클래스의 정적 메서드를 사용할 때
    - 클래스의 인스턴스를 최초로 만들 때

     

    자바에서는 클래스 초기화 블록과 인스턴스 초기화 블록을 사용할 수 있다. 이 프로젝트에서는 JPA의 생명주기 이벤트를 활용한 초기화 블록을 사용했다.

    @PrePersist
    public void prePersist() {
        if (this.orderId == null) {
            this.orderId = UUID.randomUUID();
        }
        if (this.status == null) {
            this.status = OrderStatus.WAITING;
        }
    }

    이 메서드는 JPA 엔티티가 저장되기 전에 자동으로 호출되어 초기화 작업을 수행한다.

    4. final 키워드

    • final 키워드가 나타날 수 있는 곳은 객체 지향 언어의 구성 요소(클래스, 변수, 메서드) 뿐이다.
    • 클래스에 final이 붙으면 상속을 허락하지 않겠다는 의미다.
    • 변수에 final이 붙었다면 변경 불가능한 상수가 된다.
      • 정적 상수는 선언 시에, 또는 정적 생성자에 해당하는 static 블록 내부에서 초기화가 가능하다.
      • 객체 상수 역시 선언 시에, 또는 객체 생성자 또는 인스턴스 블록에서 초기화할 수 있다.
      • 지역 상수는 선언 시에, 또는 최초 한 번만 초기화가 가능하다.
    • 메서드가 final이라면 최종이니 재정의, 즉 오버라이딩을 금지하게 된다.

    final 키워드는 변수, 메서드, 클래스에 적용하여 변경 불가능성을 보장한다.

     

    1. 서비스 클래스의 의존성 주입에 final 키워드 사용

    @Service
    @RequiredArgsConstructor
    @Slf4j
    public class OrderService {
        private final OrderRepository orderRepository;
        private final OrderDomainService orderDomainService;
        private final CompanyClient companyClient;
        private final DeliveryClient deliveryClient;
        
        // ... 메서드
    }

    2. 상수 정의에 final 키워드 사용

    public class KafkaTopics {
        public static final String ORDER_CREATED = "order-created";
        public static final String DELIVERY_CREATED = "delivery-created";
        public static final String DELIVERY_STATUS_CHANGED = "delivery-status-changed";
    }

    5. instanceof 연산자

    • 인스턴스: 클래스를 통해 만들어진 객체
    • instanceof 연산자: 만들어진 객체가 특정 클래스의 인스턴스인지 물어보는 연산자
      • 인터페이스의 구현 관계에서도 동일하게 적용된다.
      • 결과로 true 또는 false를 반납한다.
      • LSP(리스코프 치환 원칙)를 어기는 코드에서 주로 나타나는 연산자이기에 리팩토링의 대상이 아닌지 검토해야 한다.

    instanceof 연산자는 객체가 특정 타입인지 검사하는 데 사용된다. 예외 처리 로직에서 주로 사용된다.

    try {
        // ... 코드
    } catch (Exception e) {
        if (!(e instanceof CustomConflictException)) {
            throw new CustomConflictException("배송 정보 조회 중 오류가 발생했습니다: " + e.getMessage());
        }
        throw e;
    }

    이 코드는 예외가 CustomConflictException 타입인지 확인하고, 그렇지 않은 경우 새로운 CustomConflictException 예외로 래핑한다.

    6. package 키워드

    • package 키워드는 네임스페이스(이름공간)를 만들어주는 역할을 한다.
    • 특별히 하는 일은 없다.

    자바에서 패키지는 관련 클래스를 그룹화하고 네임스페이스를 제공한다. 이 프로젝트에서는 계층형 패키지 구조가 사용된다.

    com._hateam.common                       // 공통 모듈
    com._hateam.common.config                // 설정 클래스
    com._hateam.common.dto                   // DTO 클래스
    com._hateam.common.entity                // 엔티티 클래스
    com._hateam.common.event                 // 이벤트 클래스
    com._hateam.common.exception             // 예외 클래스
    
    com._hateam.order                        // 주문 모듈
    com._hateam.order.application.dto        // DTO 클래스
    com._hateam.order.application.service    // 애플리케이션 서비스
    com._hateam.order.domain.model           // 도메인 모델
    com._hateam.order.domain.repository      // 저장소 인터페이스
    com._hateam.order.domain.service         // 도메인 서비스

    이 패키지 구조는 DDD(Domain-Driven Design) 원칙에 따라 도메인, 애플리케이션, 인프라스트럭처 등의 관심사를 분리하여 코드의 가독성과 유지보수성을 높였다.

    7. interface 키워드와 implements 키워드

    • 인터페이스는 public 추상 메서드와 public 정적 상수만 가질 수 있다.
    • 인터페이스는 추상 메서드와 정적 상수만 가질 수 있기에 따로 메서드에 pubilc과 abstract, 속성에 public과 static, final을 붙이지 않아도 자동으로 자바가 붙여준다.

    인터페이스는 클래스가 구현해야 하는 메서드의 계약을 정의한다. 이 프로젝트에서는 다양한 인터페이스와 구현체가 있다.

     

    1. 저장소 인터페이스와 구현체

    public interface OrderRepository {
        Order save(Order order);
        Optional<Order> findById(UUID orderId);
        // ... 기타 메서드
    }
    
    @Repository
    @RequiredArgsConstructor
    public class OrderRepositoryImpl implements OrderRepository {
        private final JpaOrderRepository jpaOrderRepository;
        
        @Override
        public Order save(Order order) {
            return jpaOrderRepository.save(order);
        }
        
        // ... 기타 메서드 구현
    }

    2. Feign 클라이언트 인터페이스

    @FeignClient(name = "company-service", url = "${services.company.url}")
    public interface CompanyClient {
        @GetMapping("/companies/{companyId}")
        ResponseDto<CompanyDto> getCompanyById(@PathVariable("companyId") UUID companyId);
        
        // ... 기타 메서드
    }

    3. Spring 설정을 위한 인터페이스 구현

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Override
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
            resolvers.add(userHeaderInfoArgumentResolver());
        }
        
        // ... 기타 메서드
    }

    8. this 키워드

    • this: 객체가 자기 자신을 지칭할 때 쓰는 키워드
    • 지역 변수와 속성(객체 변수, 정적 변수)의 이름이 같은 경우 지역 변수가 우선한다.
    • 객체 변수와 이름이 같은 지역 변수가 있는 경우 객체 변수를 사용하려면 this를 접두사로 사용한다.
    • 정적 변수와 이름이 같은 지역 변수가 있는 경우 정적 변수를 사용하려면 클래스명을 접두사로 사용한다.

    this 키워드는 현재 객체를 참조한다. 이 프로젝트에서는 다양한 용도로 사용된다.

     

    1. 필드 접근 시 명확성을 위해

    @PrePersist
    public void prePersist() {
        if (this.orderId == null) {
            this.orderId = UUID.randomUUID();
        }
    }

    2. 자기 자신을 다른 객체에 전달

    public void addOrderProduct(OrderProduct orderProduct) {
        this.orderProducts.add(orderProduct);
        orderProduct.setOrder(this);  // 자기 자신을 OrderProduct에 전달
    }

    3. 메서드 체이닝을 위한 자기 자신 반환

    public void delete(String deletedBy) {
        this.deletedBy = deletedBy;
        this.deletedAt = LocalDateTime.now();
    }

    9. super 키워드

    • super: 단일 상속만을 지원하는 자바에서 바로 위 상위 클래스의 인스턴스를 지칭하는 키워드
    • super 키워드로 바로 위의 상위 클래스 인스턴스에는 접근할 수 있지만 super.super 형태로 상위의 상위 클래스의 인스턴스에는 접근이 불가능하다.

    super 키워드는 부모 클래스의 멤버에 접근하는 데 사용된다. 이 프로젝트에서는 명시적인 super 키워드 사용은 없지만, 상속 관계가 있다.

    public class Order extends Timestamped {
        // ... 필드 및 메서드
    }

    Order 클래스는 Timestamped의 모든 메서드와 필드를 상속받는다. 예를 들어, Timestamped의 delete 메서드를 Order 객체에서 직접 호출할 수 있다.

     

    이 글은 『스프링 입문을 위한 자바 객체 지향의 원리와 이해』 책을 학습한 내용을 정리한 것입니다.
    Comments