ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] 절차지향적인 자바 코드
    Java 2025. 12. 10. 18:02

    Java, Kotlin 등과 같이 흔히 객체지향 프로그래밍 언어를 사용하더라도 무조건 객체지향적인 코드가 나오는 것은 아닙니다. 코드 작성자가 절차지향(procedure oriented)적인 사고를 가지고 있다면 절차지향적인 코드가 나올 수 있습니다. 이번 Java 코드가 절차지향적인지, 객체지향적인지 판단하는 기준을 중심적으로 알아보겠습니다.

    1. 절차지향 프로그래밍(Procedural Programming)이란 무엇인가?

    '절차지향(procedure oriented)'이라는 단어를 들으면 무엇이 가장 먼저 떠오르시나요? 많은 개발자분들이 "위에서 아래로 코드가 순서대로 실행되는 것"이라고 답하곤 합니다. 하지만 이것은 반은 맞고 반은 틀린 설명입니다.

    엄밀히 말해 위에서 아래로 흐르는 것은 순차적(Sequential)인 흐름이며, 절차지향(Procedural)의 핵심은 단어 그대로 '프로시저(Procedure)'에 있습니다.

    1) 절차지향의 핵심: 프로시저(Procedure)

    절차지향 프로그래밍(Procedural Programming)에서 '절차'는 순서를 의미하는 것이 아니라, 함수(Function), 서브루틴(Subroutine), 메서드(Method)와 같은 기능 단위의 모듈을 의미합니다.

    즉, 절차지향 프로그래밍이란 "수행해야 할 문제를 작은 단위의 '기능(프로시저)'들로 나누고, 이 프로시저들의 호출 흐름을 통해 프로그램을 구성하는 방식"을 말합니다.

    • 데이터와 기능의 분리: 절차지향에서는 데이터를 보관하는 변수(Data)와 데이터를 처리하는 함수(Procedure)가 분리되어 있습니다.
    • 중심: '데이터'보다는 '프로세스(로직의 흐름)'가 중심이 됩니다.

    2) 많이 하는 오해: 순차지향 vs 절차지향

    이 부분을 명확히 짚고 넘어가는 것이 중요합니다.

    • 순차지향 (Sequential Programming):
      • 코드가 작성된 순서 그대로(Line by Line) 실행되는 방식입니다.
      • goto 문 등을 사용하지 않는다면, 물리적인 코드의 순서와 논리적인 실행 순서가 일치합니다.
      • 코드의 흐름 제어(Flow Control)보다는 나열에 가깝습니다.
    • 절차지향 (Procedural Programming):
      • 순차적인 처리를 포함하되, 이를 '함수(프로시저)'라는 단위로 묶어서 관리합니다.
      • 단순히 위에서 아래로 흐르는 것이 아니라, 어떤 함수를 언제 호출하느냐에 따라 프로그램의 흐름이 결정됩니다.
      • 코드의 재사용성모듈화를 활용합니다.

    2. 절차지향적인 자바 코드

    다음은 절차지향적인 자바 코드의 예시이고 다음과 같은 흐름을 가집니다.

    1. 사용자(Member)가 잔액을 가지고 있습니다.
    2. 상품(Product)은 재고와 가격을 가지고 있습니다.
    3. 주문(Order)이 발생하면 회원의 잔액은 줄고, 상품의 재고는 감소하며, 주문 내역이 생성되어야 합니다.

    Member.java

    package com.example.proceduresample;
    
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    import lombok.Setter;
    
    @AllArgsConstructor
    @Getter
    public class Member {
        private Long id;
        private String name;
        @Setter
        private int money; // 잔액
    
    
    }

     

    Order.java

    package com.example.proceduresample;
    
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    @AllArgsConstructor
    public class Order {
        @Getter
        private Long id;
        private Long memberId;
        private Long productId;
        @Getter
        private String status; // "COMPLETE", "CANCEL"
    
    
        @Override
        public String toString() {
            return "Order{id=" + id + ", memberId=" + memberId + ", productId=" + productId + ", status='" + status + "'}";
        }
    }

     

    Product.java

     

    package com.example.proceduresample;
    
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    import lombok.Setter;
    
    @Getter
    @AllArgsConstructor
    public class Product {
        private Long id;
        private String name;
        private int price;
        @Setter
        private int stock; // 재고
    
    
    
    }

     

    OrderRepository.java

    package com.example.proceduresample;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.stereotype.Repository;
    
    
    @Repository
    public class OrderRepository {
    
        // DB 역할을 하는 Map
        private Map<Long, Member> members = new HashMap<>();
        private Map<Long, Product> products = new HashMap<>();
        private Map<Long, Order> orders = new HashMap<>();
        private long orderIdSequence = 1L;
    
        public OrderRepository() {
            // 더미 데이터 초기화
            members.put(1L, new Member(1L, "홍길동", 50000));
            products.put(1L, new Product(1L, "기계식키보드", 35000, 10));
        }
    
        public Member findMemberById(Long id) {
            return members.get(id);
        }
    
        public Product findProductById(Long id) {
            return products.get(id);
        }
    
        public void save(Order order) {
            orders.put(order.getId(), order);
        }
    
        public Long generateOrderId() {
            return orderIdSequence++;
        }
    }

     

    OrderService.java

     

    package com.example.proceduresample;
    
    import org.springframework.stereotype.Service;
    import lombok.RequiredArgsConstructor;
    import org.springframework.transaction.annotation.Transactional;
    
    
    @Service
    @RequiredArgsConstructor
    public class OrderService {
    
        private final OrderRepository orderRepository;
    
        /**
         * 주문 프로세스 (절차지향적 흐름)
         */
        @Transactional
        public void createOrder(Long memberId, Long productId) {
            System.out.println("\n============== [주문 프로세스 시작] ==============");
    
            // 1. [데이터 조회]
            System.out.println("[1. 데이터 조회] DB에서 회원과 상품 정보를 가져옵니다.");
            Member member = orderRepository.findMemberById(memberId);
            Product product = orderRepository.findProductById(productId);
    
            System.out.println("   -> 조회된 회원: " + member.getName() + " (잔액: " + member.getMoney() + ")");
            System.out.println("   -> 조회된 상품: " + product.getName() + " (가격: " + product.getPrice() + ", 재고: " + product.getStock() + ")");
    
            // 2. [검증 로직]
            System.out.println("[2. 유효성 검증] 재고와 잔액을 확인합니다.");
            if (product.getStock() <= 0) {
                throw new RuntimeException("!! 예외 발생: 재고가 부족합니다.");
            }
    
            if (member.getMoney() < product.getPrice()) {
                throw new RuntimeException("!! 예외 발생: 잔액이 부족합니다.");
            }
            System.out.println("   -> 검증 통과: 구매 가능한 상태입니다.");
    
            // 3. [비즈니스 로직 수행 및 상태 변경]
            System.out.println("[3. 데이터 조작] 서비스가 직접 객체의 값을 계산하고 변경(Setter)합니다.");
    
            // 재고 감소 처리
            int currentStock = product.getStock();
            int nextStock = currentStock - 1;
            product.setStock(nextStock); // 서비스가 주입
            System.out.println("   -> 상품 재고 변경: " + currentStock + "개 => " + nextStock + "개");
    
            // 잔액 차감 처리
            int currentMoney = member.getMoney();
            int nextMoney = currentMoney - product.getPrice();
            member.setMoney(nextMoney); // 서비스가 주입
            System.out.println("   -> 회원 잔액 변경: " + currentMoney + "원 => " + nextMoney + "원");
    
            // 4. [주문 생성 및 저장]
            System.out.println("[4. 결과 저장] 주문 정보를 생성하고 DB에 반영합니다.");
            Long orderId = orderRepository.generateOrderId();
            Order order = new Order(orderId, member.getId(), product.getId(), "COMPLETED");
    
            orderRepository.save(order);
    
            System.out.println("============== [주문 프로세스 종료] ==============\n");
            System.out.println("최종 주문 결과: Order ID=" + orderId + ", Status=" + order.getStatus());
        }
    }

     

     

    AppRunner.java

    package com.example.proceduresample;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.stereotype.Component;
    
    @Component
    public class AppRunner implements CommandLineRunner {
    
        private final OrderService orderService;
    
        public AppRunner(OrderService orderService) {
            this.orderService = orderService;
        }
    
        @Override
        public void run(String... args) throws Exception {
            try {
                System.out.println("=== 로직 실행 시작 ===");
                orderService.createOrder(1L, 1L);
                System.out.println("=== 로직 실행 종료 ===");
            } catch (Exception e) {
                System.out.println("에러 발생: " + e.getMessage());
            }
        }
    }

    실행 결과

     

    3. 코드에서 발견되는 절차지향 프로그래밍의 4가지 특징

    방금 작성한 OrderService와 Member, Product 코드에는 다음과 같은 절차지향 프로그래밍의 전형적인 특징들이 있습니다.

     

    1) 데이터와 로직의 분리 (Separation of Data and Logic)

    가장 큰 특징은 데이터를 담는 객체데이터를 처리하는 객체가 물리적으로 완전히 나뉘어 있다는 점입니다.

    • Data: Member, Product, Order 클래스 (상태만 존재, 행위 없음)
    • Logic: OrderService 클래스 (상태 없음, 행위만 존재)

    마치 C언어의 struct(구조체)에 데이터를 담고, 별도의 함수가 그 구조체를 인자로 받아 처리하는 방식과 동일합니다. 자바 진영에서는 이를 '빈약한 도메인 모델(Anemic Domain Model)'이라고 부릅니다.

    3개의 클래스(Member, Product, Order)는 스스로 행위를 하지 않고 오직 데이터를 담는 용도로만 사용되고 있습니다. 이로 인해 데이터 객체들이 마치 OrderService의 메서드를 성공적으로 수행하기 위한 객체처럼 보이기까지 합니다.

    그 이유는 무엇일까요? 바로 비즈니스 로직을 수행해야 할 '책임'이 각 도메인 객체(Class)에게 있는 것이 아니라, OrderService 내부의 함수들에 전가되었기 때문입니다. 이는 결과적으로 비즈니스 로직 처리를 위한 행위와 데이터가 한 곳에 모이지 못해 응집도(Cohesion)가 낮아졌음을 의미합니다.

    2) 수동적인 데이터 객체 (Passive Object)

    객체지향의 핵심인 '캡슐화'가 깨져 있습니다. 이는 객체가 자신의 상태를 스스로 관리하고 보호해야 할 책임을 잃어버렸기 때문입니다.

    • 현상: Member 객체는 자신의 돈(money)이 차감되는지, 더해지는지, 혹은 0원이 되는지 전혀 알지 못합니다. 데이터에 대한 통제권(책임)을 잃고 완전히 수동적인 존재가 되었습니다.
    • 증거: Setter 메서드가 무분별하게 열려 있어 외부(OrderService)에서 언제든지 값을 덮어쓸 수 있습니다.
    // 객체가 "돈을 지불해"라는 요청 처리에 대한 책임이 없고
    // 서비스에 있는 함수가 "돈을 지불해"라는 요청 처리에 대한 책임이 있습니다.
    member.setMoney(nextMoney);
    • 데이터를 가진 객체가 스스로 판단하고 행동하는 것이 아니라, 외부 로직에 의해 그저 조작당하고 있을 뿐입니다.

    3) 로직의 집중화 (Centralized Logic)

    모든 비즈니스 책임이 서비스 레이어(OrderService)의 특정 메서드(createOrder) 하나에 과도하게 집중되어 있습니다.

    • 현상: createOrder 메서드는 모든 일을 처리하는 책임을 가지고 행동합니다. 데이터를 불러오고(get), 검사하고(if), 계산하고, 저장합니다.
    • 문제점: 적절히 분산되어야 할 책임이 한 곳에 뭉쳐 있습니다. 이로 인해 로직이 조금만 복잡해져도 메서드의 크기가 비대해지고 유지보수가 어려워집니다. 반면, 정작 중요한 도메인 객체(Member, Product)들은 아무런 책임도 지지 않은 채, 데이터 저장하는 용도로만 쓰입니다.

    4) getter/setter의 남발

    getter/setter를 많이 사용하다보면 개발자가 객체지향적인 사고를 하는 데에 방해가 됩니다. 이는 데이터와 로직이 한 곳에 모이지 못해 응집도(Cohesion)가 낮아지면서 발생하는 필연적인 부작용입니다.

    • 코드 패턴:
      1. get으로 값을 꺼내온다. (product.getStock())
      2. 서비스 내부에서 로직을 수행한다. (-1)
      3. set으로 값을 다시 집어넣는다. (product.setStock())
    • 분석: 로직(Service)이 데이터(Entity)를 가지고 있지 않기 때문에, 기능을 수행하기 위해서는 끊임없이 데이터를 물어보고 가져와야 합니다.
    • 결과: 이 과정에서 데이터 객체와 서비스 로직 사이의 결합도(Coupling)가 불필요하게 높아집니다. 만약 Product의 데이터 구조(변수명이나 타입)가 바뀌면, 이를 get/set 하는 서비스 코드까지 줄줄이 수정해야 하는 문제가 발생합니다.

     

    오늘은 자바(Java)와 같은 객체지향 언어를 사용하면서도, 실제로는 어떻게 절차지향적인 코드를 작성하게 되는지 그 원인과 특징을 살펴보았습니다. 다음 포스팅에서는 이 문제들을 해결하고, 객체지향적인 코드로 리팩토링하는 과정을 알아보겠습니다.

     

    [Java] 절차 지향적인 자바 코드를 객체 지향적이게 바꾸기

    저번 글에서는 절차지향과 절차지향적으로 작성된 자바 코드를 알아보았습니다. 이번 글에서는 절차지향적인 자바 코드를 어떻게 객체지향적으로 바꾸는지 알아보겠습니다. 이 글에서 가장 중

    msj9965.tistory.com

     

Designed by MSJ.