Post View

MVC는 알겠는데 Service와 DAO는 뭘까?

저는 스프링을 공부하면서 가장 난해했던 부분이 Service와 DAO였습니다.
MVC는 모델(실제 동작이 수행되는 부분), 뷰(화면에 그려지는 부분), 컨트롤러(모델과 뷰를 제어하는 부분)으로 나뉘어서 이해하는데 크게 어려움이 없었지만,
Service와 DAO는 MVC처럼 많이 다루지 않는 것 같은데 많은 스프링 프로그램 로직에서 이와같은 구조를 사용하고 있었습니다.
일부 책에서는 Controller에서 Service Interface를 통해 Service Implement를 사용하고, 또 Service Implement에서  DAO Interface를 통해 DAO Implement를 사용하는 한다고만 나와있더라구요.

제가 이해가 되지 않았던 부분은 크게 두가지였습니다.
왜 Service와 DAO라는걸 또 만들어서 나누어야하는지와 Controller, Service, DAO는 각각 어떤 코드들이 들어있어야 하는지였습니다.
사실 DAO는 "데이터에 직접적으로 접근하는 역할"이라는 것만으로 충분히 이해가 됐지만, Service의 경우 검색할 때마다 "비즈니스 로직"이라는 말만 나오더라구요.

당시에 한참을 검색하고 알아보니 어느정도 이해가 됐습니다.
먼저 Service와 DAO로 나뉘는 것은 "Layered Architecture"라는 아키텍쳐로 Presentation Layer, Business Layer, Persistence Layer, Database Layer로 나뉘어져 있으며, 대형 프로젝트에서 역할에 따라 분류함으로써 각 로직에 대해 복잡성을 줄일 수 있고 테스트 하기 용이합니다.

Spring의 경우에도 Presentation Layer는 View와 Controller, Business Layer는 Service, Persistence Layer는 DAO, Database Layer는 말 그대로 데이터베이스 부분으로 해당 아키텍쳐에 맞게 나뉘어져있더군요.
해당 아키텍쳐에 대한 내용은 위에서 말씀드린 Layered Architecture Pattern으로 검색하시면 많은 자료가 있으니 참고하시기 바랍니다.

그렇다면 Controller, Service, DAO에는 어떤 내용의 로직이 들어가야할까요?
예시를 들어보겠습니다.

제 지갑에는 돈이 만원 있습니다.
이 돈을 은행에 맡길 생각입니다.

그렇다면 먼저 은행에 가야겠죠?
저는 국민은행으로 가서 제 돈 5000원을 입금하려 합니다.

이 때 통장을 두고 와서 무통장 입금을 해야하는데, 입출금전표(무통장입금 시 사용하는 용지)를 적어서 은행원에게 무통장입금 요청을 합니다.
은행원은 입출금전표와 제가 건넨 금액을 확인하고 입출금 관리 시스템을 통해 입금 처리 합니다.

입출금 관리 시스템에서는 입금 내역은 데이터베이스에 추가하고 문자메시지를 통해 저에게 입금완료 문자를 전송합니다.
입출금 관리 시스템에서 입금이 완료되었다면 은행원은 입금이 완료되었다는 메시지를 확인하고, 저에게 입금이 완료되었다고 안내합니다.

저는 이렇게 돈을 은행에 맡길 수 있었습니다.

이를 Layered Architecture Pattern을 이용하여 코드로 작성해보겠습니다.
(별거 없을 줄 알았는데, 작성해보니 뭔가 많아졌네요...)

본인(컨트롤러)

package net.kurien.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class KurienController {
    @Autowired
    private 은행원인터페이스 은행원;

    public String 저축(Model model) {
        String 계좌번호 = "0000-1111-2222";
        int 내돈 = 10000;

        입출금전표 전표 = new 입출금전표(계좌번호, 5000);

        try {
            int 거스름돈 = 은행원.입금(전표, 내돈);

            내돈 = 거스름돈;

            return "success";
        } catch(은행강도Exception e) {
            //도망친다.
            return "run";
        } catch(Exception e) { //예상하지 못한 오류
            return "그때 가서 생각한다";
        }
    }
}

입출금전표(DTO)

package net.kurien.demo;

public class 입출금전표 {
    private String 계좌;
    private int 금액;

    입출금전표(String 계좌, int 금액) {
        this.계좌 = 계좌;
        this.금액 = 금액;
    }

    public String get계좌() {
        return 계좌;
    }

    public int get금액() {
        return 금액;
    }
}

은행원의 업무(Service Interface)

package net.kurien.demo;

public interface 은행원인터페이스 {
    int 입금(입출금전표 전표, int 입금할돈);
//    int 출금(입출금전표 전표, int 출금할돈);
//    int 이체(입출금전표 전표, int 이체할돈);
}

은행원(Service)

package net.kurien.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;

@Service
public class 국민은행원 implements 은행원인터페이스 {
    @Autowired
    private 입출금관리시스템인터페이스 입출금관리시스템;

    @Override
    public int 입금(입출금전표 전표, int 입금할돈) {
        if(전표 == null) {
            throw new 전표미입력Exception("저기 있는 입출금전표 작성해오세요.");
        }

        if(전표.get계좌() == null || 전표.get계좌().equals("") || 전표.get금액() <= 0) {
            throw new 전표오작성Exception("여기 여기 잘못 적으셨어요. 다시 적어주세요.");
        }

        if(전표.get금액() > 입금할돈) {
            throw new 입금할돈부족Exception("입금할 돈이 " + 전표.get금액() - 입금할돈 + "원 부족한데요?");
        }

        // 이 부분은 은행원이 입력할 내용
        String 입금지점 = "국민은행한국지점";
        LocalDate 입금시간 = LocalDate.now();

        입출금관리시스템입력양식 입력양식 = new 입출금관리시스템입력양식(전표.get계좌(), 전표.get금액(), 입금시간, 입금지점);

        try {
            입출금관리시스템.입금(입력양식);
        } catch(은행전산마비Exception e) {
            throw new 은행원당황Exception("죄송한데 은행 전산이 마비돼서요;; 지금은 입금이 안될 것 같습니다;;");
        }

        return 입금할돈 - 전표.get금액();
    }
}

입출금관리시스템에 입력하는 양식(DTO)

package net.kurien.demo;

import java.time.LocalDateTime;

public class 입출금관리시스템입력양식 {
    private String 계좌;
    private int 금액;
    private String 지점;
    private LocalDateTime 시간;

    입출금관리시스템입력양식(String 계좌, int 금액, String 지점, LocalDateTime 시간) {
        this.계좌 = 계좌;
        this.금액 = 금액;
        this.지점 = 지점;
        this.시간 = 시간;
    }

    public String get계좌() {
        return 계좌;
    }

    public int get금액() {
        return 금액;
    }

    public String get지점() {
        return 지점;
    }

    public LocalDateTime get시간() {
        return 시간;
    }
}

은행원이 사용하는 시스템 기능(Service Interface)

package net.kurien.demo;

public interface 입출금관리시스템인터페이스 {
    public boolean 입금(입출금관리시스템입력양식 입력양식);
}

은행원이 사용하는 시스템(Service)

package net.kurien.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.sql.SQLException;

@Service
public class 국민은행입출금관리시스템 implements 입출금관리시스템인터페이스 {
    @Autowired
    private 입출금관리DAO인터페이스 입출금관리DAO;

    @Autowired
    private 입출금안내서비스인터페이스 입출금안내서비스;

    @Override
    public boolean 입금(입출금관리시스템입력양식 입력양식) {
        try {
            입출금관리DAO.insert(입력양식);
        } catch(SQLException e) {
            throw 은행전산마비Exception("DB가 펑하고 터져서 전산이 마비되었습니다.");
        }   
        try {
            // 입출금안내서비스 클래스는 추가 작성하기가 귀찮아서 생략했습니다...
            입출금안내서비스.입금알림전송(입력양식.get시간() + " 입금 " + 입력양식.get금액() + " (" + 입력양식.get지점() + ")");
        } catch(Exception e) {
            //입금알림이 나가지 않았다는 로그 발생
            //입금 알림이 나가진 않았으나, 치명적이지 않으므로 로그만 남김.
        }
        
        return true;
    }
}

시스템이 사용가능한 DAO 기능(DAO Interface)

package net.kurien.demo;

public interface 입출금관리DAO인터페이스 {
    public boolean insert(입출금관리시스템입력양식 입력양식);
}

Interface를 구현한 DAO 기능(DAO)

package net.kurien.demo;

import org.springframework.stereotype.Repository;

@Repository
public class 입출금관리DAO implements 입출금관리DAO인터페이스 {
    @Override
    public boolean insert(입출금관리시스템입력양식 입력양식) {
        은행계좌Entity 은행계좌 = new 은행계좌Entity(입력양식.get계좌(), 입력양식.get금액(), 입력양식.get지점(), 입력양식.get시간());
        // return 은행데이터베이스.insert(은행계좌); 이와같은 형태로 DB에 Insert
        return true;
    }
}

실제 DB에 저장될 table의 Row 데이터(Entity)

package net.kurien.demo;

import java.time.LocalDateTime;

public class 은행계좌Entity {
    private String 계좌;
    private int 금액;
    private String 지점;
    private LocalDateTime 시간;
    
    은행계좌Entity(String 계좌, int 금액, String 지점, LocalDateTime 시간) {
        this.계좌 = 계좌;
        this.금액 = 금액;
        this.지점 = 지점;
        this.시간 = 시간;
    }

    public String get계좌() {
        return 계좌;
    }

    public int get금액() {
        return 금액;
    }

    public String get지점() {
        return 지점;
    }

    public LocalDateTime get시간() {
        return 시간;
    }
}

이렇게 예시를 코드로 작성해보았습니다.
결국 위에서 말하는 비즈니스 로직에 해당하는 부분은 "국민은행원""국민은행입출금시스템"인데요.
결국 해당 "업무를 담당하는 부분에 대한 로직"을 비즈니스 로직이라고 하며 이 부분을 우리는 서비스라고 합니다.

컨트롤러는 서비스에게 특정 업무를 요청하고, 서비스는 업무를 요청하며 필요한 자료를 DAO에게 요청하거나, 업무를 통해 나온 자료를 DAO를 통해 저장합니다.
예시를 보고 나서 이 내용을 보니 이해가 되시나요?

여기서 추가적으로 Service와 DAO에서 Interface를 사용하는 이유는 Controller(본인)는 Service(담당자)를 통해 입금 처리를 하지만 실제 담당자가 어떤방식으로 처리하는지는 저로써는 알 필요가 없기 때문입니다.
만약 국민은행원이 아니라 신한은행원이라고 하더라도 동일한 업무를 한다면(은행원인터페이스를 구현하였다면) 저는 신한은행에서도 같은 업무를 볼 수 있겠죠?
이러한 이유로 Controller와 실제 국민은행원 사이를 은행원인터페이스로 분리하고 추후 신한은행원 클래스가 추가된다면 국민은행원을 신한은행원으로 변경하는 것만으로 프로그램 수정이 끝나게 됩니다.(프로그램 수정으로 인한 영향범위가 적어짐)

마찬가지로 DAO의 경우에는 "은행데이터베이스.insert(은행계좌);" 부분을 텍스트 파일로 저장할 것인지, 오라클 DB를 사용할 것인지 용도에 따라 DAO를 교체하여 사용하면 됩니다.

솔직히 위의 예시로도 많이 미흡하다고 생각하지만, 제가 이런 부분을 이해하려고 했을 때 실제 프로그램에 맞는 예제를 보기가 어려워 이해하는데 많이 힘들었습니다.
부족한 예제이지만 이런 개념적인 부분을 이해하는데 조금이나마 도움이 되셨길 바라겠습니다.

혹시라도 위의 내용 중 틀린 부분이나, 미흡한 사항은 아래 댓글로 작성하여 주시면 빠르게 수정하도록 하겠습니다!

Comments