본문 바로가기

국비 교육

2020.11.5 일자 수업 : DAO interface, 커넥션 객체 공유, 로그인

 실습 - DAO Interface 

 

MariaDB를 사용하는 dao.mariadb 패키지와 Oracle을 사용하는 dao.oracle 패키지를 만들었다고 가정할 때, 기존에 사용하고 있는 패키지 클래스에서 다른 패키지의 클래스들로 사용 DAO를 변경할 때마다 DAO를 사용하는 코드를 모두 수정해야 한다. 이 불편함을 해소하기 위해 DAO 클래스에 대한 사용 규칙, 즉 인터페이스를 정의하여 클래스 대신 인터페이스를 사용하도록 한다. 

 

일단, mariaDB를 사용하고 있는 DAO 클래스는 com.eomcs.pms.dao.mariaDB에 모두 집어넣고, com.eomcs.pms.dao 패키지에는 DAO 인터페이스를 집어넣을 것이다. 일단 BoardDao를 인터페이스로 바꾸어 com.eomcs.pms.dao에 넣어주고 기존의 클래스는 com.eomcs.pms.dao.mariadb에 넣어둔다.

public interface BoardDao {

  public int insert(Board board) throws Exception;
  public int delete(int no) throws Exception;
  public Board findByNo(int no) throws Exception;
  public List<Board> findAll() throws Exception;
  public int update(Board board) throws Exception;
  
}

인터페이스와 그를 구현하는 클래스의 이름을 지정할 때에는

  • 패키지만 달리하고, 이름을 똑같이 짓는 방법
  • 구현체의 이름을 '인터페이스의 이름 + impl'라고 짓는 방법
  • 인터페이스에서는 특정되지 않는 구현체만의 특징을 클래스의 접두사로 지정하여, '접두사+인터페이스'로 짓는 방법
    (ex- MariaDBBoardDao)

우리는 뒤에 접미사로 Impl을 붙이는 방식으로 구현체의 이름을 지정할 것이다.

public class BoardDaoImpl implements BoardDao {

이제 App.java에서 각각의 Command 객체에게 넘겨주는 Dao 객체들을 생성할 때, XxxDaoImpl로 바꿔준다.

    BoardDao boardDao = new BoardDaoImpl();
    MemberDao memberDao = new MemberDaoImpl();
    ProjectDao projectDao = new ProjectDaoImpl();
    TaskDao taskDao = new TaskDaoImpl();

이제 각각의 Command 객체들은 인터페이스 변수에 저장된 인터페이스 구현체를 사용하게 될 것이고, mariaDB 이외의 다른 DBMS에 대한DAO 클래스를 사용하고자 할 때, App 클래스에서 위의 코드만을 수정하면 된다.


 실습 - 커넥션 객체 공유하기 

 

각 DAO 클래스에서 DB 커넥션을 생성하지 않고, 공유하는 방법도 사용할 수 있다. App을 시작하는 시점에서 Connection을 생성하고 준비할 수 있도록 AppInitListener 클래스contextInitialized 메서드에서 Connection을 생성하고, context에 객체를 넣어준다. 그리고 App에 끝나는 시점에서 Connection 객체를 닫아준다.

public class AppInitListener implements ApplicationContextListener {
  @Override
  public void contextInitialized(Map<String,Object> context) {
    System.out.println("프로젝트 관리 시스템(PMS)에 오신 걸 환영합니다!");
    
    try {
      Connection con = DriverManager.getConnection(
          "jdbc:mysql://localhost:3306/studydb?user=study&password=1111");
      context.put("con", con);
    } catch (Exception e) {
      System.out.println("DB 커넥션을 준비하는 중에 오류 발생");
      e.printStackTrace();
    }
  }

  @Override
  public void contextDestroyed(Map<String,Object> context) {
    System.out.println("프로젝트 관리 시스템(PMS)을 종료합니다!");
    
    try {
      Connection con = (Connection) context.get("con");
      con.close();
    } catch (Exception e) {
      
    }
  }
}

이렇게 하면 App 클래스의 service 메서드에서 notifyApplicationContextListenerOnServiceStarted();를 호출하는 코드 밑에 Connection 객체를 context에서 꺼내 각 DAO 구현체의 생성자 파라미터로 넘겨준다.

  public void service() throws Exception {

    notifyApplicationContextListenerOnServiceStarted();

    Map<String,Command> commandMap = new HashMap<>();
    Connection con = (Connection) context.get("con");

    BoardDao boardDao = new BoardDaoImpl(con);
    MemberDao memberDao = new MemberDaoImpl(con);
    ProjectDao projectDao = new ProjectDaoImpl(con);
    TaskDao taskDao = new TaskDaoImpl(con);

그리고 각 DAO 구현체로 가서 Connection 인스턴스 필드를 만들고, Connection 객체를 받아 필드에 저장하는 생성자를 만든다.

public class BoardDaoImpl implements BoardDao {
  
  Connection con;
  
  public BoardDaoImpl(Connection con) {
    this.con = con;
  }

그리고 각 DAO 구현체에서 매번 Connection을 생성하는 코드를 삭제해주면 된다.

 public int insert(Board board) throws Exception {
    try (PreparedStatement stmt = con.prepareStatement(
            "insert into pms_board(title,content,writer) values(?,?,?)")) {
      stmt.setString(1, board.getTitle());
      stmt.setString(2, board.getContent());
      stmt.setInt(3, board.getWriter().getNo());
      return stmt.executeUpdate();
    }
  }

  @Override
  public int delete(int no) throws Exception {
    try (PreparedStatement stmt = con.prepareStatement("delete from pms_board where no=?")) {
      stmt.setInt(1, no);
      return stmt.executeUpdate();
    }
  }

 실습 - 트랜잭션 

 

ProjectAddCommand의 Insert 메서드에서는 pms_project에서 프로젝트 정보를 등록하고 난 뒤 pms_member_project에 정보를 등록한다. 그런데 pms_project에서 프로젝트 정보가 등록되었으나 pms_member_project에서 정보 등록에 실패하면, 이미 pms_project에 들어간 정보와 연결된 정보가 pms_member_project에는 저장되지 못했으므로 데이터에 결함이 발생한다.

 

따라서 pms_member_project에서 정보를 입력하는 도중에 오류가 발생하면 pms_project에서 입력된 기록도 전으로 다시 무를 수 있도록 오토 커밋을 false 상태로 지정해야 하고, 모든 테이블에서의 연결된 정보 저장이 완료되는 시점에서 commit을 해야 한다.

  @Override
  public int insert(Project project) throws Exception {
    con.setAutoCommit(false);
    try {
      try (PreparedStatement stmt = con.prepareStatement(
          "insert into pms_project(title,content,sdt,edt,owner)"
              + " values(?,?,?,?,?)",
              Statement.RETURN_GENERATED_KEYS)) {

        stmt.setString(1, project.getTitle());
        stmt.setString(2, project.getContent());
        stmt.setDate(3, project.getStartDate());
        stmt.setDate(4, project.getEndDate());
        stmt.setInt(5, project.getOwner().getNo());
        stmt.executeUpdate();

        try (ResultSet keyRs = stmt.getGeneratedKeys()) {
          keyRs.next();
          project.setNo(keyRs.getInt(1));
        }
      }

      try (PreparedStatement stmt2 = con.prepareStatement(
          "insert into pms_member_project(member_no, project_no) values(?,?)")) {
        for (Member member : project.getMembers()) {
          stmt2.setInt(1, member.getNo());
          stmt2.setInt(2, project.getNo());
          stmt2.executeUpdate();
        }
      }
      con.commit();
      return 1;
    } catch (Exception e) {
      con.rollback();

      throw e;
    } finally {

      con.setAutoCommit(true);
    }
  }

 실습 - 로그인 

 

DBMS에서 회원 정보를 조회하여 간단한 로그인 기능을 한번 만들어보자. 일단 LoginCommand를 추가하여 다음과 같이 execute 메서드를 구현한다.

public class LoginCommand implements Command {

  MemberDao memberDao;

  public LoginCommand(MemberDao memberDao) {
    this.memberDao = memberDao;
  }

  @Override
  public void execute() {
    System.out.println("[로그인]");

    String email = Prompt.inputString("이메일? ");
    String password = Prompt.inputString("암호? ");
    try {
      Member member = memberDao.findByEmailPassowrd(email, password);
      if (member == null) {
        System.out.println("사용자 정보가 맞지 않습니다.");
      } else {
        System.out.printf("%s 님 반갑습니다.", member.getName());
      }
    } catch (Exception e) {
      System.out.println("로그인 중 오류 발생!");
      e.printStackTrace();
    }
  }
}

여기서 사용한 MemberDaofindByEamilPassword를 추가하고, MemberDaoIpml에서 해당 메서드를 구현한다.

public interface MemberDao {
  public int insert(Member member) throws Exception;
  public int delete(int no) throws Exception;
  public Member findByNo(int no) throws Exception;
  public Member findByName(String name) throws Exception;
  public List<Member> findAll() throws Exception;
  public int update(Member member) throws Exception;
  public Member findByEmailPassowrd(String email, String password) throws Exception;
}
  public Member findByEmailPassowrd(String email, String password) throws Exception {
    try (PreparedStatement stmt = con.prepareStatement(
        "select no, name, email, photo, tel, cdt"
            + " from pms_member"
            + " where email= ? and password=password(?)")) {

      stmt.setString(1, email);
      stmt.setString(2, password);

      try (ResultSet rs = stmt.executeQuery()) {
        if (rs.next()) {
          Member member = new Member();
          member.setNo(rs.getInt("no"));
          member.setName(rs.getString("name"));
          member.setEmail(rs.getString("email"));
          member.setPhoto(rs.getString("photo"));
          member.setTel(rs.getString("tel"));
          member.setRegisteredDate(rs.getDate("cdt"));
          return member;
        } else {
          return null;
        }
      }
    }

로그인은 이렇게 구현되었으나 이대로는 로그인하기 전이나 후의 기능적 차이가 없다. 따라서 로그인 후에 게시물을 등록하면 무조건 그 게시물의 작성자는 로그인이 된 회원으로 지정하고자 한다. 그러려면 LogInCommand에서 가져온 회원 정보(Member 객체)를 BoardAddCommand도 공유하고 있어야 한다. 이를 위해서 이 프로젝트에서 가장 공통적으로 힙에 공유되고 있는 컬렉션 객체인 context에 이 Member 객체를 담아서 각 Command가 공유할 수 있도록 할수가 있다. 

 

일단 Command 구현체들이 context의 주소를 받아 사용할 수 있도록 Command 인터페이스에서 execute 메서드의 파라미터로 Map 객체를 추가한다. 이에 따라 이 Command의 모든 구현체의 execute 메서드에 Map 객체를 파라미터로 받도록 수정한다.

public interface Command {  
  void execute(Map<String, Object> context);
}

그리고 LoginCommand의 execute 메서드에서 파라미터로 받은 context에 로그인을 성공한 회원 객체를 추가하는 코드를 마지막에 추가한다.

  @Override
  public void execute(Map<String, Object> context) {
    System.out.println("[로그인]");

    String email = Prompt.inputString("이메일? ");
    String password = Prompt.inputString("암호? ");
    try {
      Member member = memberDao.findByEmailPassowrd(email, password);
      if (member == null) {
        System.out.println("사용자 정보가 맞지 않습니다.");
      } else {
        System.out.printf("%s 님 반갑습니다.", member.getName());
      }
      
      context.put("loginUser", member);
    } catch (Exception e) {
      System.out.println("로그인 중 오류 발생!");
      e.printStackTrace();
    }
  }

그리고 이렇게 로그인된 회원 정보가 추가된 context를 BoardAddCommand가 받아 execute 메서드를 실행할 때, 그 회원을 키를 통해 꺼내어 새로 생성된 게시물의 작성자로 지정해주면 된다.

  public void execute(Map<String, Object> context) {
    System.out.println("[게시물 등록]");

    try {
      Board board = new Board();
      board.setTitle(Prompt.inputString("제목? "));
      board.setContent(Prompt.inputString("내용? "));

      
      Member loginUser = (Member) context.get("loginUser");
      board.setWriter(loginUser);
      boardDao.insert(board);
      System.out.println("게시글을 등록하였습니다.");

    } catch (Exception e) {
      System.out.println("게시글 등록 중 오류 발생!");
      e.printStackTrace();
    }
  }