본문 바로가기

국비 교육

2020.11.17 일자 수업 : Business Layer

 실습 - Business Layer 분리 

 

현재 Command에서 UI처리 작업과, 업무 로직 처리 작업(업무에 필요한 데이터 처리를 수행하는 응용프로그램의 일부)을 모두 수행하고 있다. 따라서 이것을 high-cohesion을 실현하기 위해 이 비즈니스 로직 역할을 Service 객체에게 위임하고, Command는 UI처리만 할 것이다. 따라서 Command와 Prompt 는 UI에 관한 로직으로 presentation Layer에 속하고 우리가 이번 프로젝트가 새로 생성할 Service 객체들은 Business Layer에 속한다. 반면, DAO/MyBatis/DBMS는 Peresistence Layer에 속한다. 즉, 우리의 프로젝트는 다음과 같이 나눠진다.

  • presenatation Layer : 사용자와 소통하여 데이터를 입출력하는 층
  • Business Layer : 업무에 필요한 데이터 처리를 수행하는 층
  • Persistence Layer : 데이터를 저장, 수정, 관리하는 층

MVC 구조에서는 Business Layer와 Persistence Layer를 묶어서 model이라 부르고 Presentation Layer를 Controller/view 라고 부른다.

 

그런데 우리는 Persistence Layer를 분리했을 때, Dao 인터페이스와 그의 구현체들을 만들었던 것처럼 Service 인터페이스를 만들고, 이를 구현하는 클래스(Concrete Class)들은 각각의 경우와 상황에 따라 버전별로 만들 것이다.

 

또한 각 계층은 그 다음의 계층에 구현된 객체들을 다중으로 사용할 수가 있지만, 한 계층 안에서 한 객체가 같은 계층의 객체를 사용하는 구조는 부적절하다. 그 계층에서 각 클래스들이 서로 관계를 가져 서로에게 종속되면 각 객체들의 자유도가 떨어지기 때문이다.

ProjectService - delete()

ProjectDeleteCommand부터 Presentation Layer와 business Layer를 분리해보자.

 

ProjectService 인터페이스는 다음과 같다.

public interface ProjectService {
  int delete(int no) throws Exception;
}

그리고 기존의 ProjectDeleteCommand에서 UI를 다루는 코드 이외의 비즈니스 로직을 다루는 코드는 다음과 같다. 즉, Command 객체에서 비즈니스 로직은, 데이터를 처리하기 위해 DAO 클래스들에게 작업을 지시하고, 트랜잭션을 다루는 코드를 말한다.

try {
      factoryProxy.startTransaction();
      taskDao.deleteByProjectNo(no);

      if (projectDao.delete(no) == 0) {
        System.out.println("해당 번호의 프로젝트가 존재하지 않습니다.");
        return;
      }
      System.out.println("프로젝트를 삭제하였습니다.");
      
      factoryProxy.commit();

    } catch (Exception e) {
      System.out.println("프로젝트 삭제 중 오류 발생!");
      e.printStackTrace();
      factoryProxy.rollback();
    } finally {
      factoryProxy.endTransaction();
    }

이 코드를 그대로 가져와서 DefaultProjectService라는 ProjectService 구현체의 delete 메서드에 넣어주고, 이 코드가 비즈니스 로직 작업만 온전히 수행할 수 있도록, 적절히 수정한다. 수정 내용은 다음과 같다.

  • 사용자에게 메시지를 출력하는 코드를 삭제한다.
  • 작업을 하는 와중에 오류가 발생하면, rollback는 하지만, 오류를 처리하지 않고 그대로 호출자에게 던진다.
public class DefaultProjectService implements ProjectService {
  
  TaskDao taskDao;
  ProjectDao projectDao;
  SqlSessionFactoryProxy factoryProxy;
  
  public DefaultProjectService(TaskDao taskDao, ProjectDao projectDao, SqlSessionFactoryProxy factoryProxy) {
    this.taskDao = taskDao;
    this.projectDao = projectDao;
    this.factoryProxy = factoryProxy;
  }

  @Override
  public int delete(int no) throws Exception {
    try {
      factoryProxy.startTransaction();
      taskDao.deleteByProjectNo(no);
      int count = projectDao.delete(no);
      factoryProxy.commit();
      return count;

    } catch (Exception e) {
      factoryProxy.rollback();
      throw e;
    } finally {
      factoryProxy.endTransaction();
    }
  }
}

이렇게 하면 이제 Command에서 ProjectService를 사용하게 한다. 일단 기존에 작업을 수행할 때 필요했던 DAO와 트랜잭션 관련 필드 대신, ProjectService 필드에 저장하여 사용한다. delete 메서드에서 작업을 직접 DAO에게 지시하고, 트랜잭션을 관리하는 대신, ProjectService의 메서드를 호출하는 코드로 수정한다.

public class ProjectDeleteCommand implements Command {
  ProjectService projectService;

  public ProjectDeleteCommand(ProjectService projectService) {
    this.projectService = projectService;
  }

  @Override
  public void execute(Map<String,Object> context) {
    
    System.out.println("[프로젝트 삭제]");
    int no = Prompt.inputInt("번호? ");

    String response = Prompt.inputString("정말 삭제하시겠습니까?(y/N) ");
    if (!response.equalsIgnoreCase("y")) {
      System.out.println("프로젝트 삭제를 취소하였습니다.");
      return;
    }

    try {
      if (projectService.delete(no) == 0) {
        System.out.println("해당 번호의 프로젝트가 존재하지 않습니다.");
        return;
      }
      System.out.println("프로젝트를 삭제하였습니다.");
      
    } catch (Exception e) {
      System.out.println("프로젝트 삭제 중 오류 발생!");
      e.printStackTrace();
    }
  }
}

이렇게 Command를 Service 객체를 사용하는 코드로 바꿔주었다면, AppInitContextListener에서 Service 구현체를 생성하여, ProjectDeleteCommand의 생성자로 넘겨준다.

public class AppInitListener implements ApplicationContextListener {
  @Override
  public void contextInitialized(Map<String,Object> context) {
    System.out.println("프로젝트 관리 시스템(PMS)에 오신 걸 환영합니다!");

    try {
      SqlSessionFactoryProxy sqlSessionFactory = new SqlSessionFactoryProxy(new SqlSessionFactoryBuilder().build(
          Resources.getResourceAsStream("com/eomcs/pms/conf/mybatis-config.xml")));

      BoardDao boardDao = new BoardDaoImpl(sqlSessionFactory);
      MemberDao memberDao = new MemberDaoImpl(sqlSessionFactory);
      ProjectDao projectDao = new ProjectDaoImpl(sqlSessionFactory);
      TaskDao taskDao = new TaskDaoImpl(sqlSessionFactory);
      
      ProjectService projectService = new DefaultProjectService(taskDao, projectDao, sqlSessionFactory);
      .
      .
      .
      commandMap.put("/project/delete", new ProjectDeleteCommand(projectService));   

그런데 Dao 구현체에서도 비즈니스 로직이 일부 남아있다. DAO의 delete 메서드에서 Project 정보를 삭제하는데, 이와 관련된 팀원 정보를 함께 지우기를 결정하는 코드 자체가 비즈니스 로직이다.

  @Override
  public int delete(int no) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      sqlSession.delete("ProjectDao.deleteMembers", no);

      int count = sqlSession.delete("ProjectDao.delete", no);

      return count;
    }
  }

이것을 하나의 메서드에서 다른 데이터 삭제 작업을 멋대로 결정하지 않도록, 팀원 정보를 삭제하는 코드를 별도의 메서드로 분리한다. DAO의 메서드는 아주 단순한 작업을 하나만 해야 한다.

  @Override
  public int delete(int no) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.delete("ProjectDao.delete", no);
    }
  }
  
  @Override
  public int deleteMembers(int projectNo) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.delete("ProjectDao.deleteMembers", projectNo);
    }
  }

DAO에서 어떤 메서드를 사용하여 어떤 작업을 수행할 것인지는 Service 객체에서 결정해야 한다. 즉, DAO가 아닌 Service에서 위의 두 개의 메서드를 호출함으로써 두 개의 작업을 한번에 실행한다.

  @Override
  public int delete(int no) throws Exception {
    try {
      factoryProxy.startTransaction();
      taskDao.deleteByProjectNo(no);
      projectDao.deleteMembers(no);
      int count = projectDao.delete(no);
      factoryProxy.commit();
      return count;

    } catch (Exception e) {
      factoryProxy.rollback();
      throw e;
    } finally {
      factoryProxy.endTransaction();
    }
  }
}

ProjectService - add()

다음은 ProjectAddCommand에서 분리한 Business Layer 작업을 ProjectService에서  add 메서드로 구현하여 그 안에 넣어주는 과정이다.

 

다만, Service 객체에서 메서드명으로 insert 대신 add 라는 이름을 사용하는 이유는 보통 Service 객체의 메서드 이름은 보통 비즈니스 업무 관련 용어를 사용하는 반면, DAO객체의 메서드명은 데이터 관련 용어를 사용하기 때문이다. 따라서 Service와 DAO의 메서드명이 조금씩 다를 수 있다. 

package com.eomcs.pms.service;

import com.eomcs.pms.domain.Project;

public interface ProjectService {
  int delete(int no) throws Exception;
  int add(Project project) throws Exception;
}

 

ProjectService 인터페이스를 정의했다면, 이제 이 구현체에서 add 메서드를 구현하고, ProjectAddCommand에서 비즈니스 로직을 뽑아, 메서드 몸체에 둔다. ProjectAddCommand에서 추출할만한 비즈니스 로직에 관한 코드는 ProjectDao에게 Project 정보를 insert하는 작업을 지시하는 코드 한 줄 밖에 없다.

  @Override
  public int add(Project project) throws Exception {
    projectDao.insertMembers(project.getNo());
    return projectDao.insert(project);
  }

반면, 이 ProjectDao의 insert 메서드에서도 남아있는 비즈니스 로직이 있다. 아래에 보이는 insert 메서드에서는 프로젝트 정보를 입력하는 SQL문과 이 프로젝트의 팀원 정보를 입력하는 SQL문을 실행하는 코드가 있으며, 이것은 DAO 객체가 가져야할 단순한 데이터 처리 작업을 수행하는 권한을 넘어선 작업 과정이라고 할 수 있다. 

  @Override
  public int insert(Project project) throws Exception {

    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {

      int count = sqlSession.insert("ProjectDao.insert", project);

      sqlSession.insert("ProjectDao.insertMembers", project);

      return count;
    }
  }

두 개의 데이터 작업을 하는 이 메서드를 두 개의 메서드로 분리한다. 즉, 팀원정보를 입력하는 코드를 insertMembers라는 별도의 메서드로 만드는 것이다.

  @Override
  public int insert(Project project) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.insert("ProjectDao.insert", project);
    }
  }
  
  @Override
  public int insertMembers(Project project) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.insert("ProjectDao.insertMembers", project);
    }
  }

이제 ProjectService의 Delete 메서드와 마찬가지로, ProjectService 구현체의 add 메서드에서 두 개의 DAO 메서드를 호출하고, 트랜잭션도 관리한다.

  @Override
  public int add(Project project) throws Exception {
    try {
      factoryProxy.startTransaction();
      projectDao.insert(project);
      projectDao.insertMembers(project);
      factoryProxy.commit();
      return 1;

    } catch (Exception e) {
      factoryProxy.rollback();
      throw e;
    } finally {
      factoryProxy.endTransaction();
    }
  }

이렇게 ProjectService 구현체에서 add 메서드를 구현했다면, Command에서 이 객체를 사용하여 add 메서드를 호출한다.

public class ProjectAddCommand implements Command {

  ProjectService projectService;
  
  public ProjectAddCommand(ProjectService projectService) {
    this.projectService = projectService;
  }
  .
  .
  .
  projectService.add(project);

또한 ProjectAddCommand에서도 회원을 이름으로 찾는 작업을 수행하는 코드가 있다. 이것도 MemberService 구현체로 분리시켜줘야한다. 일단 MemberService 인터페이스를 정의하고 이 안에 이름에 해당하는 문자열을 주면 그 이름에 맞는 멤버들을 찾아주는 list라는 추상 메서드를 정의한다.

public interface MemberService {
  List<Member> list(String name) throws Exception;
}

그리고 DefaultMemberService라는 구현체에서 이 메서드를 구현하며, ProjectAddCommand에서 MemberDao에 findByName을 호출하는 코드를 가져와 몸체에 집어넣는다.

public class DefaultMemberService implements MemberService {
  
  MemberDao memberDao;
  
  public DefaultMemberService(MemberDao memberDao) {
    this.memberDao = memberDao;
  }

  @Override
  public List<Member> list(String name) throws Exception {
    return memberDao.findByName(name);
  }
}

그런데 현재 MemberDao의 findByName은 하나의 멤버 객체만을 리턴하므로 이것을 수정한다.

  @Override
  public List<Member> findByName(String name) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("MemberDao.findByName", name);
    }
  }

이제 ProjectAddCommand에서는 ProjectService 뿐만 아니라 MemberService도 사용하므로, 각각에 대해 두 개의 필드를 관리한다. 이제, MemberDao를 바로 사용하는 코드를  MemberService의 list 메서드를 호출하는 코드로 바꿔준다. 다만 이때,  MemberService의 list 메서드는 List<Member>를 리턴함을 염두에 두어 리턴 타입으로 List<Member>를 준비한다. 그리고 한 Project 객체의 필드가 모두 유효하게 초기화되면 이것을 ProjectDao에 바로 넘기지 않고, ProjectService의 add를 호출하여 정보를 대신 입력하게 한다. 이것이 하나의  Command에서 여러개의 Service 객체를 사용하는 모양새이다. 

public class ProjectAddCommand implements Command {

  ProjectService projectService;
  MemberService memberService;
  
  public ProjectAddCommand(ProjectService projectService, MemberService memberService) {
    this.projectService = projectService;
    this.memberService = memberService;
  }

  @Override
  public void execute(Map<String,Object> context) {
    System.out.println("[프로젝트 등록]");

    try {
      Project project = new Project();
      project.setTitle(Prompt.inputString("프로젝트명? "));
      project.setContent(Prompt.inputString("내용? "));
      project.setStartDate(Prompt.inputDate("시작일? "));
      project.setEndDate(Prompt.inputDate("종료일? "));

      Member loginUser = (Member) context.get("loginUser");
      project.setOwner(loginUser);

      List<Member> members = new ArrayList<>();
      while (true) {
        String name = Prompt.inputString("팀원?(완료: 빈 문자열) ");
        if (name.length() == 0) {
          break;
        } else {
          List<Member> list = memberService.list(name);
          if (list.size() == 0) {
            System.out.println("등록된 회원이 아닙니다.");
            continue;
          }
          members.add(list.get(0));
        }
      }

      project.setMembers(members);

      projectService.add(project);

      System.out.println("프로젝트가 등록되었습니다!");

    } catch (Exception e) {
      System.out.println("프로젝트 등록 중 오류 발생!");
      e.printStackTrace();
    }
  }
}

ProjectAddCommand가 수정되었다면 AppInitListener에서 MemberService 구현체를 생성하고, ProejctAddCommand의 생성자 파라미터로 추가해준다.

public class AppInitListener implements ApplicationContextListener {
  @Override
  public void contextInitialized(Map<String,Object> context) {
    System.out.println("프로젝트 관리 시스템(PMS)에 오신 걸 환영합니다!");

    try {
      SqlSessionFactoryProxy sqlSessionFactory = new SqlSessionFactoryProxy(new SqlSessionFactoryBuilder().build(
          Resources.getResourceAsStream("com/eomcs/pms/conf/mybatis-config.xml")));

      BoardDao boardDao = new BoardDaoImpl(sqlSessionFactory);
      MemberDao memberDao = new MemberDaoImpl(sqlSessionFactory);
      ProjectDao projectDao = new ProjectDaoImpl(sqlSessionFactory);
      TaskDao taskDao = new TaskDaoImpl(sqlSessionFactory);
      
      ProjectService projectService = new DefaultProjectService(taskDao, projectDao, sqlSessionFactory);
      MemberService membeService = new DefaultMemberService(memberDao);
      .
      .
      .
      commandMap.put("/project/add", new ProjectAddCommand(projectService, memberService));

ProjectService - list()

다음으로는 이제 ProjectListCommand에서 비즈니스 로직을 뽑아 ProjectService의 list 메서드로 구현하는 과정이다.

 

일단 ProjectService 인터페이스에서 list 메서드를 추가한다.

public interface ProjectService {
  int delete(int no) throws Exception;
  int add(Project project) throws Exception;
  List<Member> list() throws Exception;
}

그런데  ProjectListCommand에서 비즈니스로직을 뽑아 list 메서드의 몸체에 두어야 할 것은 결국 ProjectDao의 findAll 메서드를 호출하는 코드 한줄 뿐이다.

  @Override
  public List<Project> list() throws Exception {
    return projectDao.findAll();
  }

굳이 이런 코드를 메서드로 만들기 위해 인터페이스와 클래스, 그리고 메서드를 모두 만들어야 하는 것일까? 만약 각각의 커맨드 객체가 상황에 따라 Service 객체를 쓰거나 Dao 객체를 쓴다면 프로그래밍에 일관성이 없어 유지보수가 어렵다. 커맨드 객체가 서비스 객체를 사용하기로 했으면 어떤 작업을 수행하던지 간에 일관되게 서비스 객체를 사용하는 것이 유지보수하기에 더 낫다. 그래서 서비스 객체의 메서드에서 특별히 할 일이 없다하더라도, 커맨드 객체가 일관성있게 작업을 수행할 수 있도록 중간에서 DAO 객체의 메서드를 호출해 주는 것이다.

 

그런데 ProjectListCommand 말고도 ProjectSearchCommand에서 ProjectService의 list() 메서드를 사용할 수 있게 하려면 몇가지 수정 사항이 필요하다.

 

일단 ProjectService 인터페이스에서 list 메서드에 문자열 파라미터를 추가한다.

public interface ProjectService {
  int delete(int no) throws Exception;
  int add(Project project) throws Exception;
  List<Project> list(String keyword) throws Exception;
}

 ProjectService 구현체에서도 마찬가지로 파라미터를 추가하고, 몸체에 findAll 메서드로 이 파라미터를 그대로 넘긴다.

  @Override
  public List<Project> list(String keyword) throws Exception {
    return projectDao.findAll(keyword);
  }

ProjectDao의 findAll 메서드에서도 마찬가지로 문자열 파라미터를 추가하고, 대신 findByKeyword 메서드를 삭제한다.

public interface ProjectDao {
  int insert(Project project) throws Exception;
  int delete(int no) throws Exception;
  Project findByNo(int no) throws Exception;
  List<Project> findAll(String keyword) throws Exception;
  List<Project> findByDetailKeyword(Map<String,Object> keywords) throws Exception;
  int update(Project project) throws Exception;
  int deleteMembers(int projectNo) throws Exception;
  int insertMembers(Project project) throws Exception;
}

ProjectDao 구현체의 findAll 메서드에도 문자열 파라미터를 추가한 후, select 메서드의 파라미터로 문자열을 넘겨준다.

 

  @Override
  public List<Project> findAll(String keyword) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("ProjectDao.findAll", keyword);
    }
  }

그리고 Mapper 파일에서 findAll SQL문을 다음과 같이 바꿔준다. 다음은 if 태그를 사용하여 파라미터로  문자열을 받았을 경우에만 select에 where 절을 추가하는 동적 SQL문이다.

  <select id="findAll" resultMap="ProjectMap" parameterType="string">
    <include refid="sql1"/>
        <if test="keyword != null">
        where
        p.title like concat('%', #{keyword}, '%')
        or m.name like concat('%', #{keyword}, '%')
        or m2.name like concat('%', #{keyword}, '%')
        </if>
    order by p.no desc
  </select>

 그리고 ProjectListCommand에서는 ProjectService의 list 메서드를 호출할 때, null을 넘겨준다.

 List<Project> list = projectService.list(null);

그리고 ProjectSearchCommand에서는 ProjectDao 대신 ProjectService을 필드로 관리하게 하고, ProjectService의 list 메서드를 사용하며 이때는 ProjectListCommand와 달리, 사용자가 입력한 문자열을 파라미터로 넘겨준다.

 @Override
  public void execute(Map<String,Object> context) {
    System.out.println("[프로젝트 검색]");

    try {
      String keyword = Prompt.inputString("검색어? ");

      List<Project> list = projectService.list(keyword);
      System.out.println("번호, 프로젝트명, 시작일 ~ 종료일, 관리자, 팀원");

      for (Project project : list) {
        StringBuilder members = new StringBuilder();
        for (Member member : project.getMembers()) {
          if (members.length() > 0) {
            members.append(",");
          }
          members.append(member.getName());
        }

        System.out.printf("%d, %s, %s ~ %s, %s, [%s]\n",
            project.getNo(),
            project.getTitle(),
            project.getStartDate(),
            project.getEndDate(),
            project.getOwner().getName(),
            members.toString());
      }
    } catch (Exception e) {
      System.out.println("프로젝트 목록 조회 중 오류 발생!");
      e.printStackTrace();
    }
  }

ProjectService - get()

이제 ProjectDetailCommand에서 비즈니스 로직을 추출하여 ProjectService의 get() 메서드로 만들 것이다.

 

일단 ProjectService 인터페이스에서 get 메서드를 추가하자.

public interface ProjectService {
  int delete(int no) throws Exception;
  int add(Project project) throws Exception;
  List<Project> list(String keyword) throws Exception;
  List<Project> list(Map<String, Object> keywords) throws Exception;
  Project get(int no) throws Exception;
}

그 후에는 ProjectService 구현체에서도 get 메서드를 구현하며, 몸체에서는 projectDao의 findByNo를 호출한다.  

  @Override
  public Project get(int no) throws Exception {
    return projectDao.findByNo(no);
  }

그런데 ProjectDetailCommand에서는 해당 프로젝트의 작업 목록을 가져오기 위해 TaskDao도 사용한 바가 있으므로 이 TaskDao에 대신 접근할 TaskService도 추가로 정의하며, 특정 프로젝트의 작업 정보들을 조회하는 listByProject 메서드를 추가한다. 파라미터는 작업들이 소속된 프로젝트의 번호를 넘겨줄 문자열을 추가한다.

public interface TaskService {
  List<Task> listByProject(int projectNo) throws Exception;

}

그리고 TaskService의 구현체에서는  Map 객체를 만들어 이 안에 파라미터로 받은 projectNo를 저장하고, 이 Map 객체를 파라미터로 넘겨주며 findAll을 호출한다.

  @Override
  public List<Task> listByProject(int projectNo) throws Exception {
    HashMap<String, Object> map = new HashMap<>();
    map.put("projectNo", projectNo);
    return taskDao.findAll(map);
  }

이렇게 TaskService의 listByProject 메서드와 ProjectService의 get 메서드가 준비되었다면, 이를 ProjectDetailCommand에서 ProjectDao와 TaskDao 대신 사용한다.

public class ProjectDetailCommand implements Command {
  ProjectService projectService;
  TaskService taskService;

  public ProjectDetailCommand(ProjectService projectService, TaskService taskService) {
    this.projectService = projectService;
    this.taskService = taskService;
  }

  @Override
  public void execute(Map<String,Object> context) {
    System.out.println("[프로젝트 상세보기]");
    int no = Prompt.inputInt("번호? ");

    try {
      Project project = projectService.get(no);
      .
      .
      .

      List<Task> tasks = taskService.listByProject(no);

이렇게 ProjectDetailCommand를 수정했다면, AppInitListener에서 TaskService 구현체를 만들고, ProjectService 구현체와 함께 해당 Command의 생성자 파라미터로 넘겨준다.

      ProjectService projectService = new DefaultProjectService(taskDao, projectDao, sqlSessionFactory);
      MemberService memberService = new DefaultMemberService(memberDao);
      TaskService taskService = new DefaultTaskService(taskDao);
      .
      .
      .
      commandMap.put("/project/add", new ProjectAddCommand(projectService, memberService));
      commandMap.put("/project/list", new ProjectListCommand(projectService));
      commandMap.put("/project/detail", new ProjectDetailCommand(projectService, taskService));

ProjectService - update()

이제 마지막으로 ProjectUpdateCommand에서 비즈니스 로직을 추출하여 ProjectService의 update 메서드로 구현해보자.

 

그런데 그전에 Project의 Mapper 파일에서 update SQL문이 사용자의 문자열 입력 여부에 상관없이 모든 컬럼 값을 입력받은대로 변경하고 있으므로, 이를 사용자가 아무것도 입력하지 않으면 해당 컬럼 값은 변경하지 않게끔 수정한다. 

  <update id="update" parameterType="project">
    update pms_project set
    <set>
      <if test="title != null">title = #{title},</if>
      <if test="content != null">content = #{content},</if>
      <if test="startDate != null">sdt = #{startDate},</if>
      <if test="endDate != null">edt = #{endDate},</if>
    </set>
    where no = #{no}
  </update>

이렇게 바꾸어줬다면, ProjectDao의 update 메서드에서 파라미터로 받은 Project 객체를 update SQL문을 실행하는 메서드의 파라미터로 넘겨준다. 그리고 deleteMembers SQL문과 inserMembers SQL문을 실행하는 코드를 별도의 메서드로 분리해준다.

  @Override
  public int update(Project project) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.update("ProjectDao.update", project);
    }
  }
  
  @Override
  public int deleteMembers(int projectNo) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.delete("ProjectDao.deleteMembers", projectNo);
    }
  }

  @Override
  public int insertMembers(Project project) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.insert("ProjectDao.insertMembers", project);
    }
  }

ProjectDao를 위와 같이 수정했다면,  이 메서드를 호출하는 ProjectService의 구현체에서는 단순히 projectDao의 update 메서드만을 호출할 것이다. 즉, ProjectUpdateCommand에서는 팀원 정보나 관리자 정보를 바꾸지 않을 것이므로 deleteMembers나 insertMembers 등의 메서드를 호출하지는 않을 것이다.

  @Override
  public int update(Project project) throws Exception {
    return projectDao.update(project);
  }

 update SQL문은 각 프로퍼티 값이 null일 경우, 정보를 바꾸지 않으므로, ProjectUpdateCommand에서 사용자가 빈문자열을 입력하지 않을 경우에만 각 필드에 입력받은 값을 저장하는 코드로 수정한다. 또한 ProjectDao 객체에 대하여 update를 호출하는 대신 projectService에 대하여 update를 호출한다.

public class ProjectUpdateCommand implements Command {
  ProjectService projectService;

  public ProjectUpdateCommand(ProjectService projectService) {
    this.projectService = projectService;
  }

  @Override
  public void execute(Map<String,Object> context) {
    System.out.println("[프로젝트 변경]");
    int no = Prompt.inputInt("번호? ");

    try {
      Project project = projectService.get(no);
      if (projectService.update(project) == 0) {
        System.out.println("해당 번호의 프로젝트가 존재하지 않습니다.");
        return;
      }

      String value = Prompt.inputString(String.format(
          "프로젝트명(%s)? ", project.getTitle()));
      if (value.length() > 0) {
        project.setTitle(value);
      }
      
      value = Prompt.inputString(String.format(
          "내용(%s)? ", project.getContent()));
      if (value.length() > 0) {
        project.setContent(value);
      }
      
      value = Prompt.inputString(String.format(
          "시작일(%s)? ", project.getStartDate()));
      if (value.length() > 0) {
        project.setStartDate(Date.valueOf(value));
      }
      
      value = Prompt.inputString(String.format(
          "종료일(%s)? ", project.getEndDate()));
      if (value.length() > 0) {
        project.setEndDate(Date.valueOf(value));
      }

      String response = Prompt.inputString("정말 변경하시겠습니까?(y/N) ");
      if (!response.equalsIgnoreCase("y")) {
        System.out.println("프로젝트 변경을 취소하였습니다.");
        return;
      }

      if (projectService.update(project) == 0) {
        System.out.println("해당 번호의 프로젝트가 존재하지 않습니다.");
      } else {
        System.out.println("프로젝트를 변경하였습니다.");
      }
    } catch (Exception e) {
      System.out.println("프로젝트 변경 중 오류 발생!");
      e.printStackTrace();
    }
  }
}

이렇게 ProjectUpdateCommand를 수정했다면 마지막으로 AppInitListener에서 ProjectService 객체를 ProjectUpdateCommand의 생성자 파라미터로 넘겨주기만 하면 된다.

commandMap.put("/project/update", new ProjectUpdateCommand(projectService));