딸기말차

[Java] 10. CRUD 실습 본문

Bootcamp/Java

[Java] 10. CRUD 실습

딸기말차 2023. 7. 5. 20:57

엔코아 플레이데이터(Encore Playdata) Backend 2기 백엔드 개발 부트캠프 (playdata.io)

 

백엔드 개발 부트캠프

백엔드 기초부터 배포까지! 매력있는 백엔드 개발자 포트폴리오를 완성하여 취업하세요.

playdata.io

 


1. CRUD 실습_2) 입력 값에 따라 작업을 수행하는 프로그램

어제 마지막에 진행하였던 실습을 강사님께서 풀이해주셨다.

혼자 진행했던 풀이와 가장 큰 차이는 클린코드를 위해 클래스를 많이 분리하신 점이었다. 

- 중복 데이터 및 query들을 상수로 저장하기 위한 interface

- DB 레코드 정보를 객체화 하여 사용하기 위한 dto 패키지 

- 출력 문구를 위한 PrintResult class

 

우선 interface를 보면, 고정값(상수)로 들어가는 변수들과 query를 작성해 implements하여 사용할 수 있도록 구현하였다.

* Oracle, MySQL, SQLite : ANSI (표준) Query
* 이 DB 들은 각각 접속방식이 다르기 때문에 driver ~ pw_mql 까지는 DB 종류마다 달라진다.
* 인터페이스는 내부 상수도 구현이 가능하고, abstract class로 바꿔 사용도 가능하다.
* 단, 추상 클래스를 사용하려면 implements 가 아니라 extends 로 바꿔 사용해야한다.
public interface Querys {
	String driver = "com.mysql.cj.jdbc.Driver";
	String dbName = "db_emp";
	String url = "jdbc:mysql://localhost:3306/" + dbName + "?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
	String id_mysql = "root";
	String pw_mysql = "0000"; // 본인 비밀번호
	
	String querySelectAll = "select * from employee";	
	String querySelectOne = "select * from employee where id = ?";
	String queryInsert = "insert into employee "
			+ "(empNo, empName, job, mgr, hireDate, sale, commission, deptNo) "
			+ "values (?, ?, ?, ?, ?, ?, 0, ?)";
	String queryUpdate = "update employee set sale = ? where seq_no = ?";
	String queryDelete = "delete from employee where job = ?";
}

두 번째로, 데이터를 객체화하여 DTO의 역할을 할 수 있게 하는 객체를 구현하였다.

* 하나의 레코드 정보를 객체화
* dto (data transfer object) : 데이터를 싣고 다니는 객체
* spring에서는 vo (value object) 라고도 한다.
public class Employee {
	private int empNo = 0; // 사원 고유번호
	private String empName = ""; // 사원이름
	private String job = ""; // 직급
	private String mgr = ""; // 직급번호
	private String hireDate = ""; // 입사일
	private int sale = 0 ; // 급여
	private int commission = 0; // 성과급
	private int deptNo = 0; // 부서번호
	
	public Employee() {
	}
	
	public Employee(int empNo, String empName, String job, String mgr, String hireDate, int sale, int commission,int deptNo) {
		this.empNo = empNo;
		this.empName = empName;
		this.job = job;
		this.mgr = mgr;
		this.hireDate = hireDate;
		this.sale = sale;
		this.commission = commission;
		this.deptNo = deptNo;
	}
    
    /*
	 * private 변수들에 대한 getter / setter
	 */
    
	@Override
	public String toString() {
		return "Employee [empNo=" + empNo + ", empName=" + empName + ", job=" + job + ", mgr=" + mgr + ", hireDate="
				+ hireDate + ", sale=" + sale + ", commission=" + commission + ", deptNo=" + deptNo + "]";
	}
}

마지막으로, 출력을 위한 PrintClass를 구현하였다.

public class PrintResult {
	public PrintResult() {
	}

	public static void printMenu() {
		System.out.println();
		System.out.println("원하는 작업 번호를 입력해 주세요..");
		System.out.println("1. 전체사원조회 / 2. 사원추가 / 3. 전사원 급여10%올림 / 4. 과장 직책 사원 삭제 / 5. 작업 종료:  ");
	}
	
	public static void printSelectAll(ArrayList<Employee> employees) {
		for (Employee e : employees)
			System.out.println(e.toString());
	}
}

해당 코드들로 변경 됨에 따라 기존에 구현했던 메서드들도 약간씩 코드가 변하게 되었다.

우선 selectAll() 메서드를 보면, 조회 결과를 ArrayList에 담아 return 후 PrintClass에서 사용하는 방식으로 변경되었다.

또한, 기존에 query를 하드코딩해 작성하던 부분이 interface에서 미리 구현한 상수로 대체되었다.

public ArrayList<Employee> selectAll() {
    ArrayList<Employee> employees = new ArrayList<Employee>();
    Statement stmt = null;
    ResultSet rs = null;	
    try {
        stmt = conn.createStatement();
        rs = stmt.executeQuery(querySelectAll);
        // empNo, empName, job, mgr, hireDate, sale, commission, deptNo
        while (rs.next()) {
            int empNo = rs.getInt("empNo");
            String empName = rs.getString("empName");
            String job = rs.getString("job");
            String mgr = rs.getString("mgr");
            String hireDate = rs.getString("hireDate");
            int sale = rs.getInt("sale");
            int commission = rs.getInt("commission");
            int deptNo = rs.getInt("deptNo");
            employees.add(new Employee(empNo, empName, job, mgr, hireDate, sale, commission, deptNo));
        }
    } catch (SQLException e) {
        System.out.println("SelectAll ERR: " + e.getMessage());
    } finally {
        close(stmt, rs);
    }
    System.out.println("데이터 전체 조회 성공");
    return employees;
}

두 번째로 update() 메서드를 보면, 기존에는 메서드 내에서 select를 통해 데이터를 조회한 후 update를 실행하도록 구현하였다.

하지만 위에 변경 된 selectAll()을 통해 데이터를 return 할 수 있게 되었기 때문에, return 한 데이터를 파라미터로 받고, 받은 데이터를 이용해 update를 실행할 수 있도록 변경되었다.

public void update(ArrayList<Employee> employees) {
    PreparedStatement pstmt = null;		
    try {
        pstmt = conn.prepareStatement(queryUpdate);

        int n = 0;
        for (int i = 0; i < employees.size(); i++) {
            int sale = employees.get(i).getSale();
            sale += (sale * 10 / 100);
            pstmt.setInt(1, sale);
            pstmt.setInt(2, i + 1);
            n += pstmt.executeUpdate();
        }
        if (n > 0)
            System.out.println(n + "개의 레코드 수정 성공");
    } catch (SQLException e) {
        System.out.println("Update ERR: " + e.getMessage());
    } finally {
        close(pstmt);
    }

}

변경 된 파일들에 따라 Main에서 실행을 위해 호출해야 되는 부분의 코드도 변경되었다. DTO로 이용하기 위한 Employee 객체를 생성해 데이터를 담고, 이 후 switch-case를 통해 구현한 메서드들을 호출하였다.

public static void main(String[] args) {
    MySQLconnector mysql = new MySQLconnector();
    mysql.connectMySQL();

    ArrayList<Employee> employees = null;

    PrintResult.printMenu();
    Scanner scan = new Scanner(System.in);

    while(true) {
        int n = scan.nextInt();
        switch (n) {
        case 1:
            employees = mysql.selectAll();
            PrintResult.printSelectAll(employees);
            PrintResult.printMenu();
            break;
        case 2:
            mysql.insert();
            PrintResult.printMenu();
            break;
        case 3:
            employees = mysql.selectAll();
            mysql.update(employees);
            PrintResult.printMenu();
            break;
        case 4:
            mysql.delete();
            PrintResult.printMenu();
            break;
        default:
            System.exit(0);
        }
    }
}

2.  CRUD 실습_3) 입력 값에 따라 작업을 수행하는 프로그램

동일하지만, 요구사항이 달라 내부 로직을 다르게 구현해야하는 실습을 진행하였다. 해당 실습의 요구사항은 다음과 같다.

입력값 1 : 전체 레코드 조회

입력값 2 : 성별이 기타인 사람 성별 수정

입력값 3 : 멤버 추가

입력값 4 : 평균 방문횟수보다 많이 방문한 사람 명단

입력값 5 : 나이 비공개인 사람들의 방문 횟수

입력값 6 : 작업종료

초기 테이블 현황

 

우선, 해당 기능을 구현하기 전에 interface에 초기 설정 값들과 query를 먼저 작성하였다.

public interface Querys {
	String driver = "com.mysql.cj.jdbc.Driver";
	String dbName = "db_bootcamp";
	String url = "jdbc:mysql://localhost:3306/" + dbName + "?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
	String id_mysql = "root";
	String pw_mysql = "0000"; // 본인 비밀번호
	
	String querySelectAll = "select * from bootcamp2";	
	String queryUpdate = "update bootcamp2 set cGenger = ? where cGenger = '기타' and cName = ?";
	String queryInsert = "insert into bootcamp2 "
			+ "(cName, cRating, cJoinDate, cLastDate, cVisitNo, cPostNo, cCommentNo, cGenger, cAge) "
			+ "values (?, ?, ?, ?, ?, ?, ?, ?, ?)";
	String querySelectOverVisitNoAvg = "select * from bootcamp2 where cVisitNo > ?";
	String querySelectClosedAge = "select * from bootcamp2 where cAge = '비공개'";
}

그리고, DTO의 역할을 해주는 Bootcamp 클래스를 구현하였다. 해당 클래스를 구현할 때 setter는 구현하지 않고, 초기 생성 시에만 데이터를 넣어줄 수 있는 생성자와 값을 가져올 수 있는 getter만 구현하였다.

왜냐하면 DTO는 DB 내의 정보를 객체화하여 계층 간 전송을 위해 사용하는데, 이 값이 setter를 통해 쉽게 바뀔 수 있다면, 문제가 발생할 수 있기 때문이다. 

public class Bootcamp {
	private String cName = ""; // 이름
	private String cRating = ""; // 멤버 등급
	private String cJoinDate = ""; // 가입일
	private String cLastDate = ""; // 최종 방문일
	private int cVisitNo = 0 ; // 방문 수
	private int cPostNo = 0; // 게시글 수
	private int cCommentNo = 0; // 댓글 수
	private String cGenger = ""; // 성별
	private String cAge = ""; // 연령대
	
	public Bootcamp() {
	}

	public Bootcamp(String cName, String cRating, String cJoinDate, String cLastDate, int cVisitNo, int cPostNo,
			int cCommentNo, String cGenger, String cAge) {
		super();
		this.cName = cName;
		this.cRating = cRating;
		this.cJoinDate = cJoinDate;
		this.cLastDate = cLastDate;
		this.cVisitNo = cVisitNo;
		this.cPostNo = cPostNo;
		this.cCommentNo = cCommentNo;
		this.cGenger = cGenger;
		this.cAge = cAge;
	}

	public String getcName() {
		return cName;
	}

	public String getcRating() {
		return cRating;
	}

	public String getcJoinDate() {
		return cJoinDate;
	}

	public String getcLastDate() {
		return cLastDate;
	}

	public int getcVistiNo() {
		return cVisitNo;
	}

	public int getcPostNo() {
		return cPostNo;
	}

	public int getcCommentNo() {
		return cCommentNo;
	}

	public String getcGenger() {
		return cGenger;
	}

	public String getcAge() {
		return cAge;
	}

	@Override
	public String toString() {
		return "Bootcamp [cName=" + cName + ", cRating=" + cRating + ", cJoinDate=" + cJoinDate + ", cLastDate="
				+ cLastDate + ", cVisitNo=" + cVisitNo + ", cPostNo=" + cPostNo + ", cCommentNo=" + cCommentNo
				+ ", cGenger=" + cGenger + ", cAge=" + cAge + "]";
	}
}

이렇게 기능 개발을 위한 초기 세팅을 마치고, 요구사항에 맞는 기능 구현을 시작하였다.

우선 첫 번째 요구사항인 전체 레코드 조회를 할 수 있는 메서드이다. 이전 실습에서 강사님께서는 setter를 통해 데이터를 넣어주었지만, 이번 실습에서는 DTO의 역할을 지키기 위해 생성자를 통하여 데이터를 저장하였다.

public ArrayList<Bootcamp> selectAll() {
		ArrayList<Bootcamp> bootcamps = new ArrayList<Bootcamp>();
		Statement stmt = null;
		ResultSet rs = null;
		try {
			stmt = conn.createStatement();
			rs = stmt.executeQuery(querySelectAll);
			
			while (rs.next()) {
				bootcamps.add(new Bootcamp(rs.getString("cName"), rs.getString("cRating"),
						rs.getString("cJoinDate"), rs.getString("cLastDate"), rs.getInt("cVisitNo"),
						rs.getInt("cPostNo"), rs.getInt("cCommentNo"), rs.getString("cGenger"),
						rs.getString("cAge")));
			}
		} catch (SQLException e) {
			System.out.println("selectAll ERR: " + e.getMessage());
		} finally {
			close(stmt, rs);
		}
		return bootcamps;
}

다음으로, 성별이 기타인 사람의 성별을 수정하는 기능을 구현하였다. 해당 기능을 구현하는 알고리즘을 살펴보면,

1. 성별이 "기타" 로 되어있는 레코드를 찾고, 그 레코드에서 이름 데이터만 뽑아 저장 후 return 한다.

2. DB에 update 쿼리를 날리며 where 절의 조건으로 성별이 "기타" 이고 , 이름이 성별 "기타" 로 되어있는 사람의 이름이라면, update를 진행한다.

해당 구조를 위해 우선 성별이 "기타" 인 사람의 이름을 저장해 return하는 메서드를 구현하였다.

public ArrayList<String> findNames() {
    ArrayList<String> names = new ArrayList<String>();
    ArrayList<Bootcamp> bootcamps = selectAll();
    for (Bootcamp b : bootcamps)
        if (b.getcGenger().equals("기타"))
            names.add(b.getcName());
    return names;
}

그리고 저장한 이름 List를 사용해, DB에 update 쿼리를 날리며 where의 조건에 맞는 사람인지 찾아, 맞다면 update를 수행하였다.

public void update(ArrayList<String> names) {
    PreparedStatement pstmt = null;
    String male = "남";
    String female = "여";
    try {
        pstmt = conn.prepareStatement(queryUpdate);
        int n = 0;
        for (String s : names) {
            if (s.equals("허찬")) {
                pstmt.setString(1, male);
                pstmt.setString(2, s);
            }
            else if (s.equals("김효경")) {
                pstmt.setString(1, female);
                pstmt.setString(2, s);
            }
            else if (s.equals("이현수")) {
                pstmt.setString(1, male);
                pstmt.setString(2, s);
            }
            else if (s.equals("박정우")) {
                pstmt.setString(1, male);
                pstmt.setString(2, s);
            }
            else if (s.equals("최호준")) {
                pstmt.setString(1, male);
                pstmt.setString(2, s);
            }
            n = pstmt.executeUpdate();				
        }
        if (n > 0)
            System.out.println("update 완료");	
    } catch (SQLException e) {
        System.out.println("update ERR: " + e.getMessage());
    } finally {
        close(pstmt);
    }
}

세 번째로, 새 멤버를 추가하는 insert() 를 구현하였다. 새 멤버를 추가하기 위해 데이터를 생성할 여러 방법이 있다.

이 때, 메서드는 동작을 위한 기능만 수행하고, 데이터 생성 및 저장은 다른 곳에서 진행되어야 한다고 생각한다.

때문에 Main 안에서 데이터를 생성하고, 해당 데이터를 파라미터로 날려 insert를 진행하였다.

Bootcamp bootcamp = new Bootcamp("이순신", "멤버", "2023.06.22.", "2023.07.10.", 36, 0, 0, "남", "50대후반");
public void insert(Bootcamp bootcamp) {
    PreparedStatement pstmt = null;
    try {
        pstmt = conn.prepareStatement(queryInsert);
        pstmt.setString(1, bootcamp.getcName());
        pstmt.setString(2, bootcamp.getcRating());
        pstmt.setString(3, bootcamp.getcJoinDate());
        pstmt.setString(4, bootcamp.getcLastDate());
        pstmt.setInt(5, bootcamp.getcVistiNo());
        pstmt.setInt(6, bootcamp.getcPostNo());
        pstmt.setInt(7, bootcamp.getcCommentNo());
        pstmt.setString(8, bootcamp.getcGenger());
        pstmt.setString(9, bootcamp.getcAge());
        int n = pstmt.executeUpdate();
        if (n > 0)
            System.out.println("insert 완료");
    } catch (SQLException e) {
        System.out.println("insert ERR: " + e.getMessage());
    } finally {
        close(pstmt);
    }
}

네 번째로, 평균 방문 횟수보다 많이 방문한 사람의 명단을 구현하였다. 해당 기능을 구현하는 과정을 살펴보면,

1. 평균 방문 횟수를 구해 return 한다.

2. 구한 평균 방문 횟수를 통해 데이터를 선별해 저장 후 return, 출력한다.

 

우선 평균 방문 횟수를 구하기 위한 메서드를 구현하였다. 이는 데이터를 전부 가져오는 메서드인 selectAll()을 활용해 구현을 진행하였다.

public float visitNoAvg() {
    int sum = 0;
    ArrayList<Bootcamp> bootcamps = selectAll();
    for (Bootcamp b : bootcamps)
        sum += b.getcVistiNo();
    return (float)sum / bootcamps.size();
}

구한 값을 insert()의 내부에서 사용해 평균 이상 방문한 사람의 이름을 key, 방문 횟수를 value로 하는 Map을 구현해 저장 후 return 하였다.

public Map<String, Integer> selectOverVisitNoAvg() {
    Map<String, Integer> map = new HashMap<String, Integer>();
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    float visit = visitNoAvg();
    System.out.println("평균 방문 횟수: " + visit);
    try {
        pstmt = conn.prepareStatement(querySelectOverVisitNoAvg);
        pstmt.setFloat(1, visit);
        rs = pstmt.executeQuery();
        while (rs.next())
            map.put(rs.getString("cName"), rs.getInt("cVisitNo"));

    } catch (SQLException e) {
        System.out.println("selectOverVisitNoAvg ERR: " + e.getMessage());
    } finally {
        close(pstmt, rs);
    }
    return map;
}

마지막으로, 나이 비공개인 사람들의 방문 횟수를 출력하는 기능을 구현하였다.

이 때 Java 쪽에선 특별히 알고리즘을 구현하진 않았고, query에서 where을 통해 조건을 줘서 데이터를 선별 후 해당 데이터의 이름을 key, 방문 횟수를 value로 하는 Map을 구현해 저장 후 return 하였다.

public Map<String, Integer> selectClosedAge() {
    Map<String, Integer> map = new HashMap<String, Integer>();
    Statement stmt = null;
    ResultSet rs = null;
    try {
        stmt = conn.createStatement();
        rs = stmt.executeQuery(querySelectClosedAge);

        while (rs.next())
            map.put(rs.getString("cName"), rs.getInt("cVisitNo"));
    } catch (SQLException e) {
        System.out.println("selectClosedAge ERR: " + e.getMessage());
    } finally {
        close(stmt, rs);
    }
    return map;
}

마지막으로 return 받은 데이터들을 이용해 출력을 위한 printClass를 구현하였다. 위에서부터 보면,

1. selectAll() 을 통해 return 받은 데이터를 파라미터로 받아 출력

2. selectOverVisitNoAvg() 을 통해 return 받은 데이터를 파라미터로 받아 출력

3. selectClosedAge() 을 통해 return 받은 데이터를 파라미터로 받아 출력

public static void printSelectAll(ArrayList<Bootcamp> bootcamps) {
    for (Bootcamp b : bootcamps)
        System.out.println(b.toString());
}

public static void printSelectOverVisitNoAvg(Map<String, Integer> map) {
    System.out.println("----- 평균보다 많이 방문한 사람 -----");
    for (Map.Entry<String, Integer> entry : map.entrySet())
        System.out.println(entry.getKey() + "의 방문 횟수: " + entry.getValue());
}

public static void printSelectClosedAge(Map<String, Integer> map) {
    System.out.println("----- 나이 비공개인 사람 -----");
    for (Map.Entry<String, Integer> entry : map.entrySet())
        System.out.println(entry.getKey() + "의 방문 횟수: " + entry.getValue());
}

해당 구현 클래스들을 Main에서 switch-case를 통해 출력하였다.

while(true) {
    PrintResult.printMenu();
    int input = scan.nextInt();
    switch (input) {
    case 1:
        PrintResult.printSelectAll(mysql.selectAll());
        break;
    case 2:
        mysql.update(mysql.findNames());
        break;
    case 3:
        mysql.insert(bootcamp);
        break;
    case 4:
        PrintResult.printSelectOverVisitNoAvg(mysql.selectOverVisitNoAvg());
        break;
    case 5:
        PrintResult.printSelectClosedAge(mysql.selectClosedAge());
        break;
    default:
        System.exit(0);
    }
}

3.  10일차 후기

 

코딩은 하는 사람마다 스타일이 많이 다르다. 보통 강사님께서 전일 실습을 내주시고 다음날 아침에 해설을 해주시는데,

내가 구현한 코드와 강사님이 구현한 코드를 비교해보면 구현하는 방식이 차이가 날 때가 많다. 강사님께서 구현하실 때 조금 더 세분화 시키며 구현하는 느낌이다.

 

앞으로 Spring 프로젝트를 진행하면, View, Controller, Service, Domain, Repository, DTO 등 여러 계층에 따라 코드를 나눠 구현해야한다. 때문에 목적에 따라 세분화를 하는 연습을 통해, Spring 프로젝트를 진행할 때도 길을 잃지 않고 구현을 할 수 있게 되야 할 것이다.

'Bootcamp > Java' 카테고리의 다른 글

[Java] 12. Mini Project_1  (0) 2023.07.09
[Java] 11. CRUD 실습 풀이  (0) 2023.07.06
[Java] 9. JDBC, CRUD  (0) 2023.07.04
[Java] 8. Generic, SOLID, Lambda, Singleton, Database  (0) 2023.07.03
[Java] 7. Thread, Collections, 외부 라이브러리  (0) 2023.07.02