6주차 과제 리팩토링

git repository : https://github.com/zezeg2/java-practice/tree/main/jdbc-practice

  • 콘솔으로부터 사용자의 입력을 받아 회원(Member)를 등록/조회/수정/삭제(CRUD)하는 프로그램 작성
  • Member
    create table member (
        id varchar(10) primary key,
        pw varchar(10)  not null,
        name varchar(10),
        phone char(11) check(phone like '010%'),
        email varchar(30) unique,
        address varchar(50),
        indate datetime
    );
  • RDBMS는 MariaDB
  • 누구나 회원등록을 할 수 있다.
  • 전체 회원정보를 조회할 수 있다.
  • 특정 회원의 정보를 조회하거나 특정유저 정보 업데이트, 삭제를 위해서는 인증의 과정이 필요하다.거창하게 인증이라 했지만 그냥 쿼리로 아디 비번 맞는지만 확인함 (아이디 및 비밀번호 일치시 서비스 실행)

패키지 구조


├── pom.xml
├── main
│   ├── java
│   │   └── template
│   │       ├── MemberRunner.java
│   │       ├── connection
│   │       │   ├── ConnectionInform.java
│   │       │   └── JDBCConnection.java
│   │       ├── domain
│   │       │   └── member
│   │       │       ├── dao
│   │       │       │   └── MemberDAO.java
│   │       │       ├── dtos
│   │       │       │   ├── AuthorizeMemberDTO.java
│   │       │       │   ├── InfoMemberDTO.java
│   │       │       │   ├── MemberDTO.java
│   │       │       │   └── UpdateMemberDTO.java
│   │       │       ├── exceptions
│   │       │       │   ├── IncorrectPasswordException.java
│   │       │       │   └── MemberNotFoundException.java
│   │       │       └── view
│   │       │           ├── CreateMemberViewImpl.java
│   │       │           ├── DeleteMemberViewImpl.java
│   │       │           ├── GetAllMemberInfoViewImpl.java
│   │       │           ├── GetMemberInfoViewImpl.java
│   │       │           ├── UpdateMemberViewImpl.java
│   │       │           └── View.java
│   │       └── tools
│   │           └── GlobalScanner.java
│   └── resources
...

pom.xml

  • 빌드 시스템 : maven
  • 필수 의존성 추가 : mariadb-client : MariaDB Connector, MariaDB전용 JDBC Driver
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>template</groupId>
        <artifactId>jdbc-practice</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
        <dependencies>
            <!-- https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client -->
            <dependency>
                <groupId>org.mariadb.jdbc</groupId>
                <artifactId>mariadb-java-client</artifactId>
                <version>3.1.0</version>
            </dependency>
        </dependencies>
    
    </project>

connectoin 패키지

ConnectionInform.java

JDBC 커넥션을 위한 Constants

public class ConnectionInform {
	public final static String DRIVER_CLASS = "org.mariadb.jdbc.Driver";
	public final static String JDBC_URL = "jdbc:mariadb://localhost:3306/memberdb";
	public final static String USERNAME = "jdbc";
	public final static String PASSWORD = "jdbc";
}

JDBCConnaction.java

  • 인스턴스 생성 없이 Connection 객체를 리턴하는 스태틱 메소드 getConnection() 선언
public class JDBCConnection {
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);
    }
}

tools 패키지

GlobalScanner.java

public class GlobalScanner implements Closeable {
    private static GlobalScanner instance = null;

    private final Scanner scanner;

    private GlobalScanner() {
        this.scanner = new Scanner(System.in);
    }

    public static GlobalScanner getInstance() {
        if (instance == null) instance = new GlobalScanner();
        return instance;
    }

    public Scanner getScanner() {
        return scanner;
    }

    public int nextNum(String comment) {
        System.out.print(comment);
        while (!scanner.hasNextInt()) {
            scanner.next();
            System.err.print("올바른 값을 입력해주세요. 재 선택 > ");
        }
        return scanner.nextInt();
    }

    public String nextString(String comment) {
        System.out.print(comment);
        return scanner.next();
    }

    public int nextNumOrCheckReplace(String comment, String check, int replace) {
        System.out.print(comment);
        while (!scanner.hasNextInt()) {
            if (scanner.next().equalsIgnoreCase(check)) return replace;
            System.err.printf("올바른 값을 입력해주세요 (종료 : '%s') 재 선택 > ", check);
        }
        return scanner.nextInt();
    }

    public String nextStringOrReplace(String comment, String check, String replace){
        System.out.print(comment);
        String input = scanner.next();
        return input.equals(check) ? replace : input;
    }

    @Override
    public void close() {
        scanner.close();
    }
}

MainClass

MemberRunner.java

  • 사용자로부터(콘솔) 입력을 받아 입력값에 따라 다른 View를 제공하며 각 View의 구현체들은 각각 run() 메서드를 구현한다.
  • View 인터페이스 구현체
    • CreateMemberViewImpl : 회원 (Member)정보를 입력받아 데이터베이스에 데이터를 삽입하는 DAO 메서드createMember(MemberDTO dto)를 호출한다
    • UpdateMemberViewImpl : id, pw를 입력받아 데이터베이스에 등록된 회원 정보를 조회하는 DAO 메서드getMember(AuthorizeMemberDTO dto) 실행 후 조회 성공시(인증O) 수정하고자 하는 데이터를 입력받아 레코드를 수정하는 DAO 메서드deleteMember(String id) 호출
    • DeleteMemberViewImpl : id, pw를 입력받아 데이터베이스에 등록된 회원 정보를 조회하는 DAO 메서드getMember(AuthorizeMemberDTO dto) 실행 후 조회 성공시(인증O) 해당 레코드를 삭제하는 DAO메서드updateMember(UpdateMemberDTO dto) 호출
    • GetMemberInfoViewImpl : id, pw를 입력받아 데이터베이스에 등록된 회원 정보를 조회하고 조회 성공시(인증O) 해당 레코드의 회원 정보를 노출하기 위한 DTOInfoMemberDTO로 Mapping 하여 이를 콘솔에 출력한다
    • GetAllMemberInfoViewImpl() : 페이지 번호를 입력받아 가입일을 기준으로 내림차순으로 조회된 레코드들을(COUNT_PER_PAGE개 조회) InfoMemberDTO 로 Mapping 한 List로 반환하는 DAO 메서드getAllMemberInfo(int page)를 실행한다. 회원 정보들을 사용자가 입력한 페이지 번호별로 출력한다.
  • RuntimeException 발생 시 (id조회 실패 및, pw 불일치)(MemberNotFoundException, IncorrectPasswordException 등) 의도된 예외 이므로 에러 메세지를 출력하고 다른 처리는 하지 않는다
  • SQLException 발생 시 Connection 문제 등 심각한 문제로 간주하여 루프를 종료시키고 메인메서드 종료
public class MemberRunner {
    public static void main(String[] args) {
        try (GlobalScanner sc = GlobalScanner.getInstance()) {
            System.out.println("===== 회원 관리 프로그램 =====");
            System.out.println("\n1. 회원정보 입력\n2. 회원정보 수정\n3. 회원 탈퇴\n4. 회원 정보 조회\n5. 전체 회원 조회\n6. 메뉴보기\n'q' => 프로그램 종료");
            while (true) {
                int select = sc.nextNumOrCheckReplace("\n메뉴 입력 > ", "q", 0);
                if (select == 0) {
                    System.out.println("====== 프로그램 종료 ======");
                    break;
                }
                try {
                    switch (select) {
                        case 1 -> CreateMemberViewImpl.getInstance().run();
                        case 2 -> UpdateMemberViewImpl.getInstance().run();
                        case 3 -> DeleteMemberViewImpl.getInstance().run();
                        case 4 -> GetMemberInfoViewImpl.getInstance().run();
                        case 5 -> GetAllMemberInfoViewImpl.getInstance().run();
                        case 6 -> System.out.print("\n1. 회원정보 입력\n2. 회원정보 수정\n3. 회원 탈퇴\n4. 회원 정보 조회\n5. 전체 회원 조회\n6. 메뉴보기\n'q' => 프로그램 종료");
                        default -> System.err.print("올바른 값을 입력해주세요. 재 선택 > ");
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                    break;
                } catch (RuntimeException e) {
                    System.out.println(e.getMessage());
                }
            }
        }
    }
}

domain.member 패키지

domain.member.dtos

  • Java14 이후 record (data class) 이용

MemberDTO.java

// 데이터베이스의 member record와 일치하는 dto
public record MemberDTO(String id, String pw, String name, String email, String phone, String address, String indate) {}

AuthorizeMemberDTO.java

//id, pw가 일치하는지 확인하기 위해 id, pw 필드만을 가짐
public record AuthorizeMemberDTO(String id, String pw) { }

InfoMemberDTO.java

// 조회시 필요한 정보만을 노출하기 위한 dto
public record InfoMemberDTO(String id, String name, String email, String phone, String address, String indate) {
    @Override
    public String toString() {
        return "InfoMemberDTO{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", phone='" + phone + '\'' +
                ", address='" + address + '\'' +
                ", indate='" + indate + '\'' +
                '}';
    }
}

UpdateMemberDTO.java

// update 를 위해 id와 일치하는 레코드의 정보를 업데이트 하기 위한 dto
public record UpdateMemberDTO(String id, String pw, String email, String phone, String address) {}

domain.member.dao

MemberDAO.java

  • 싱글톤 인스턴스로 관리
  • 메소드들은 JDBCConnaction의 getConnection 메소드를 통해 Connection 객체를 자원으로 선언하고 데이터베이스와 통신한다. try-with-resources 로 정상적으로 자원을 반납하도록 보장한다
  • 페이지네이션을 위해 한 페이지 당 출력할 레코드 수를 나타내는 COUNT_PER_PAGE 를 멤버로 가진다
public class MemberDAO {
    private static MemberDAO instance;

    public static MemberDAO getInstance() {
        if (instance == null) instance = new MemberDAO();
        return instance;
    }

    private final int COUNT_PER_PAGE = 3;

    private MemberDAO() {
    }

	// id는 unique 컬럼이므로 예외를 방지하기위해 중복확인이 요구된다. 
    // sql의 exist() 함수를 통해 효율적으로 테이블 내 id 존재유무를 확인하여 boolean 값 리턴
    public boolean isExistId(String id) throws SQLException {
        String sql = "SELECT EXISTS(SELECT 1 FROM member WHERE id = ?)";
        try (Connection con = JDBCConnection.getConnection();
             PreparedStatement pt = con.prepareStatement(sql)) {
            pt.setString(1, id);
            ResultSet rs = pt.executeQuery();
            rs.next();
            return rs.getInt(1) == 1;
        }
    }
		// email는 unique 컬럼이므로 예외를 방지하기위해 중복확인이 요구된다.  
		// id와 동일한 방법으로 존재유무 확인 후 boolan value return
    public boolean isExistEmail(String email) throws SQLException {
        String sql = "SELECT EXISTS(SELECT 1 FROM member WHERE email = ?)";
        try (Connection con = JDBCConnection.getConnection();
             PreparedStatement pt = con.prepareStatement(sql)) {
            pt.setString(1, email);
            ResultSet rs = pt.executeQuery();
            rs.next();
            return rs.getInt(1) == 1;
        }
    }
    // insert 쿼리 실행, 리턴 void
    public void createMember(MemberDTO dto) throws SQLException {
        String sql = "INSERT INTO member (id, pw, name, email, phone, address, indate) VALUES (?,?,?,?,?,?,now())";
        try (Connection con = JDBCConnection.getConnection();
             PreparedStatement pt = con.prepareStatement(sql)) {
            pt.setString(1, dto.getId());
            pt.setString(2, dto.getPw());
            pt.setString(3, dto.getName());
            pt.setString(4, dto.getEmail());
            pt.setString(5, dto.getPhone());
            pt.setString(6, dto.getAddress());
            pt.execute();
        }
    }
    // member 테이블 내 전체 레코드 수를 카운팅하여 전체 페이지 리턴
    public int countPage() throws SQLException {
        String sql = "SELECT COUNT(*) FROM member";
        try (Connection con = JDBCConnection.getConnection();
             Statement st = con.createStatement()) {
            ResultSet rs = st.executeQuery(sql);
            rs.next();
            int totalCnt = rs.getInt(1);
            return (totalCnt - 1) / COUNT_PER_PAGE + 1;
        }
    }
	// limit -> 조회쿼리의 조회레코드 수를 제한
	// offset -> 조회시 offset 값 이후부터의 데이터를 가져온다
    // limit, offset을 이용하여 페이지네이션 구현
    public List<InfoMemberDTO> getAllMemberInfo(int page) throws SQLException {
        List<InfoMemberDTO> memberList = new ArrayList<>();
        String sql = "SELECT * FROM member ORDER BY indate LIMIT ? OFFSET ?";
        try (Connection con = JDBCConnection.getConnection();
             PreparedStatement pt = con.prepareStatement(sql)) {

            int startIndex = COUNT_PER_PAGE * (page - 1);
            pt.setInt(1, COUNT_PER_PAGE);
            pt.setInt(2, startIndex);
            ResultSet rs = pt.executeQuery();
            while (rs.next()) {
                String id = rs.getString("id");
                String name = rs.getString("name");
                String email = rs.getString("email");
                String phone = rs.getString("phone");
                String address = rs.getString("address");
                String indate = rs.getString("indate");
                memberList.add(new InfoMemberDTO(id, name, email, phone, address, indate));
            }
            return memberList;
        }
    }
	// 회원의 AuthorizeMemberDTO를 인수로 받아 getMember메소드 실행 -> 
	// 반환된 MemberDTO 객체로부터 필드값을 InformMemberDto와 매핑하여InformMemberDto를 리턴한다
    public InfoMemberDTO getMemberInfo(AuthorizeMemberDTO dto) throws SQLException {
        MemberDTO member = getMember(dto);
        return new InfoMemberDTO(member.getId(), member.getName(), member.getEmail(), member.getPhone(), member.getAddress(), member.getIndate());
    }

	// UpdateMemberDTO 를 인수로 받아 dto의 id에 해당하는 레코드에 대해 업데이트 쿼리를 실행한다.
    public void updateMember(UpdateMemberDTO dto) throws SQLException {
        String sql = "UPDATE member SET pw = ?, email = ?, phone = ?, address = ? WHERE id = ?";
        try (Connection con = JDBCConnection.getConnection();
             PreparedStatement pt = con.prepareStatement(sql)) {
            pt.setString(1, dto.pw());
            pt.setString(2, dto.email());
            pt.setString(3, dto.phone());
            pt.setString(4, dto.address());
            pt.setString(5, dto.id());
            pt.executeQuery();
        }
    }
		
	// id를 인수로 받아 해당 id에 해당하는 레코드를 삭제하는 쿼리를 실행한다
    public void deleteMember(String id) throws SQLException {
        String sql = "DELETE FROM member WHERE id = ?";
        try (Connection con = JDBCConnection.getConnection();
             PreparedStatement pt = con.prepareStatement(sql)) {
            pt.setString(1, id);
            pt.executeQuery();
        }
    }
		
	// AuthorizeMemberDTO 를 인수로 받고 id, pw 모두 일치하는지 확인하고 일치 할시 MemberDTO 리턴
	// id 조회 안되면 throw MemberNotFoundException (extends RuntimeException)
	// pw 일치하지 않으면 throw IncorrectPasswordException (extends RuntimeException)
    public MemberDTO getMember(AuthorizeMemberDTO dto) throws SQLException {
        String sql = "SELECT id, pw, name, phone, email, address, indate FROM member WHERE id = ?";
        try (Connection con = JDBCConnection.getConnection();
             PreparedStatement pt = con.prepareStatement(sql)) {
            pt.setString(1, dto.id());
            ResultSet rs = pt.executeQuery();
            if (rs.next()) {
                if (rs.getString("pw").equals(dto.pw())) {
                    String id = rs.getString("id");
                    String pw = rs.getString("pw");
                    String name = rs.getString("name");
                    String email = rs.getString("email");
                    String phone = rs.getString("phone");
                    String address = rs.getString("address");
                    String indate = rs.getString("indate");

                    return new MemberDTO(id, pw, name, email, phone, address, indate);
                } else throw new IncorrectPasswordException();
            } else throw new MemberNotFoundException();
        }
    }
}

domain.member.views

View.java (Interface)

  • MemberDAO 및 GlobalScanner를 멤버로 가진다.

  • GlobalScanner로 사용자 입력을 받고, 입력받은 데이터를 DAO 메서드를 실행하는 인자로 전달한다.

  • DAO를 실행하고 반환값이 있다면 콘솔에 출력한다.

    public interface View {
        MemberDAO dao = MemberDAO.getInstance();
        GlobalScanner sc = GlobalScanner.getInstance();
        void run() throws SQLException;
    }

CreateMemberViewImpl.java

 public class CreateMemberViewImpl implements View {
     private static CreateMemberViewImpl instance;
 
     private CreateMemberViewImpl() {
     }
 
     public static CreateMemberViewImpl getInstance() {
         if (instance == null) instance = new CreateMemberViewImpl();
         return instance;
     }
 
     @Override
     public void run() throws SQLException {
 
         String id = sc.nextString("아이디 입력 > ");
         if (dao.isExistId(id)) {
             System.out.println("이미 존재하는 id 입니다.");
             return;
         }
         String email = sc.nextString("이메일 입력 > ");
         if (dao.isExistEmail(email)) {
             System.out.println("이미 존재하는 이메일 입니다.");
             return;
         }
         String pw = sc.nextString("패스워드 입력 > ");
         String name = sc.nextString("이름 입력 > ");
         String phone = sc.nextString("휴대전화 입력 > ");
         String address = sc.nextString("주소 입력 > ");
         dao.createMember(new MemberDTO(id, pw, name, email, phone, address, null));
         System.out.println("회원 등록 완료");
     }
 }

DeleteMemberViewImpl.java

 public class DeleteMemberViewImpl implements View {
     private static DeleteMemberViewImpl instance;
 
     private DeleteMemberViewImpl() {
     }
 
     public static DeleteMemberViewImpl getInstance() {
         if (instance == null) instance = new DeleteMemberViewImpl();
         return instance;
     }
 
     @Override
     public void run() throws SQLException {
         AuthorizeMemberDTO member = new AuthorizeMemberDTO(sc.nextString("아이디 입력 > "), sc.nextString("패스워드 입력 > "));
         MemberDTO authMember = dao.getMember(member);
         dao.deleteMember(authMember.id());
         System.out.println("유저정보가 삭제되었습니다.");
     }
 }

GetAllMemberInfoViewImpl.java

 public class GetAllMemberInfoViewImpl implements View {
     private static GetAllMemberInfoViewImpl instance;
 
     private GetAllMemberInfoViewImpl() {
     }
 
     public static GetAllMemberInfoViewImpl getInstance() {
         if (instance == null) instance = new GetAllMemberInfoViewImpl();
         return instance;
     }
 
     @Override
     public void run() throws SQLException {
         List<InfoMemberDTO> result = dao.getAllMemberInfo(sc.nextNum(String.format("페이지 번호 입력 (1 - %d 페이지) > ", dao.countPage())));
         result.forEach(System.out::println);
     }
 }

GetMemberInfoViewImpl.java

 public class GetMemberInfoViewImpl implements View {
     private static GetMemberInfoViewImpl instance;
 
     private GetMemberInfoViewImpl() {
     }
 
     public static GetMemberInfoViewImpl getInstance() {
         if (instance == null) instance = new GetMemberInfoViewImpl();
         return instance;
     }
 
     @Override
     public void run() throws SQLException {
         AuthorizeMemberDTO member = new AuthorizeMemberDTO(sc.nextString("아이디 입력 > "), sc.nextString("패스워드 입력 > "));
         InfoMemberDTO result = dao.getMemberInfo(member);
         System.out.println(result);
     }
 }

UpdateMemberViewImpl.java

 public class UpdateMemberViewImpl implements View {
     private static UpdateMemberViewImpl instance;
 
     private UpdateMemberViewImpl() {
     }
 
     public static UpdateMemberViewImpl getInstance() {
         if (instance == null) instance = new UpdateMemberViewImpl();
         return instance;
     }
 
     public void run() throws SQLException {
         AuthorizeMemberDTO member = new AuthorizeMemberDTO(sc.nextString("아이디 입력 > "), sc.nextString("패스워드 입력 > "));
         MemberDTO origin = dao.getMember(member);
 
         System.out.println("재설정할 항목을 입력해주세요 (enter 'p'-> 다음항목)");
         String tempEmail = sc.nextString("새로운 이메일 입력 > ");
         if (dao.isExistEmail(tempEmail)) {
             System.out.println("이미 존재하는 이메일 입니다.");
             return;
         }
         String email = tempEmail.equals("p") ? origin.email() : tempEmail;
         String pw = sc.nextStringOrReplace("새로운 패스워드 입력 > ", "p", origin.pw());
         String phone = sc.nextStringOrReplace("새로운 휴대전화 입력 > ", "p", origin.phone());
         String address = sc.nextStringOrReplace("새로운 주소 입력 > ", "p", origin.address());
         dao.updateMember(new UpdateMemberDTO(origin.id(), pw, email, phone, address));
         System.out.println("유저정보 업데이트에 성공했습니다.");
     }
 }

domain.member.exceptions 패키지

서비스 로직 중 정상적으로 일어날 수 있는 Exceptions, → 런타임예외를 상속한다

IncorrectPasswordException.java

public class IncorrectPasswordException extends RuntimeException{
    public IncorrectPasswordException() {
        super("비밀번호가 일치하지 않습니다.");
    }
}

MemberNotFoundException.java

public class MemberNotFoundException extends RuntimeException{
    public MemberNotFoundException() {
        super("존재하지 않는 아이디입니다.");
    }
}

실행결과

Senario1

유저생성 → 유저조회 실패 확인 (MemberNotFoundException , IncorrectPasswordException ) → 유저 조회 성공

Senario2

유저 정보 업데이트 → 업데이트 정보로 로그인 → 유저삭제 → 삭제확인

Senario3

메뉴 다시보기 → 전체 유저 조회 → 메뉴 이외 입력값 선택 → 종료

0개의 댓글