어제 TIL에 상세보기/검색/입력까지 연결했다고 적었는데, 그때 Service의 update(), delete()는 사실 아래처럼 예외만 던지는 껍데기였다.
@Override
public int update(BlogRequestDTO request) {
throw new UnsupportedOperationException("Unimplemented method 'update'");
}
오늘 이 두 메서드를 Dao까지 내려가서 실제로 구현했다.
update는 findAny로 찾은 blog의 필드를 직접 바꿔치기하는 방식이다.
public int update(BlogRequestDTO request) {
return blogs.stream()
.filter(blog -> blog.getBlogId() == request.getBlogId())
.findAny()
.map(blog -> {
blog.setTitle(request.getTitle());
blog.setContent(request.getContent());
return 1;
})
.orElse(0);
}
map이 원래 값을 변환하는 용도인데 여기서는 blog 필드를 직접 바꾸면서 동시에 1을 리턴하는 데 쓰였다. blog가 참조타입이라 이 안에서 값을 바꾸면 원본 리스트 안의 객체도 그대로 바뀐다는 걸 알고 나니, 왜 이 방식이 되는지 이해가 됐다. 다만 Stream의 중간 연산은 원래 부수 효과 없이 순수하게 변환만 하는 게 원칙이라, 이렇게 원본을 직접 바꾸는 건 엄밀히는 권장되는 패턴은 아니라고 한다.
어제 배운 Stream API는 컬렉션 데이터를 조회, 가공하는 파이프라인이고
IO Stream은 프로그램과 파일·콘솔 같은 외부 사이에 데이터가 오가는 통로를 뜻한다.
| 구분 | 바이트(1byte) | 문자(2byte) |
|---|---|---|
| 읽기 | InputStream / FileInputStream | Reader / FileReader |
| 쓰기 | OutputStream / FileOutputStream | Writer / FileWriter |
| 한글 처리 | 깨짐 | 안 깨짐 |
~Stream으로 끝나면 바이트 계열, ~Reader/~Writer로 끝나면 문자 계열이다.
int input = System.in.read();
System.out.println(input); // 'A' 입력하면 65가 찍힘
System.in이 InputStream이라 반환값이 char가 아니라 int로 나온다.
이걸 InputStreamReader로 감싸면 문자로 바뀌고, 다시 BufferedReader로 감싸면 readLine()으로 한 줄씩 읽을 수 있다. Scanner가 사실 내부에서 이러한 체이닝을 하고 있었다는 걸 알고 나니 좀 신기했다.
자원을 닫는 코드를 finally 안에서 또 try-catch로 감싸야 하는 게 번거로웠는데, try 괄호 안에 자원을 선언하면 블록이 끝날 때 자동으로 닫아준다.
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
System.out.println(br.readLine());
} catch (Exception e) {
e.printStackTrace();
}
AutoCloseable을 구현한 클래스만 이렇게 쓸 수 있다는 것도 배웠다.
문자열이 아니라 List<DTO> 같은 객체를 저장하려면 직렬화가 필요하다.
public class BlogResponseDTO implements Serializable {
// 필드는 그대로, implements만 추가
}
Serializable은 메서드가 하나도 없는 마킹 인터페이스라서 implements만 붙이면 된다. 컬렉션(ArrayList)은 이미 Serializable인데 그 안에 담긴 DTO까지 마킹돼 있어야 한다는 걸 놓치면 NotSerializableException이 난다고 배웠다.
public boolean saveToFile() {
String path = "./blogs.txt";
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File(path)))) {
oos.writeObject(dao.findByAll());
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
writeObject의 매개변수 타입이 Object(다형성)라서 List 통째로 넘길 수 있다.
반대로 읽어올 때는 readObject()의 반환도 Object라 캐스팅이 필요한데, 이게 어제 배운 BlogBeanFactory.getBean()이 Object를 반환하던 것과 같은 패턴이다.
public boolean loadToFile() {
String path = "./blogs.txt";
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File(path)))) {
dao.setBlogs((List<BlogResponseDTO>) ois.readObject());
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
읽어온 걸 Service가 들고 있지 않고 dao.setBlogs()로 다시 넘기는 이유는, 실제 데이터를 관리하는 영역이 Dao이기 때문이다. 프로그램을 처음 실행하면 blogs.txt가 없어서 FileNotFoundException이 나는 게 정상이라는 것도 짚어주셨다.
list는 List, read는 DTO, insert/update/delete는 int, file은 boolean처럼 컨트롤러마다 반환 타입이 다 달라서 View 쪽이 지저분했는데, code/message/data를 담는 제네릭 클래스로 감쌌다.
public class ResponseEntity<T> {
private int code;
private String message;
private T data;
}
public ResponseEntity<List<BlogResponseDTO>> list() {
return new ResponseEntity<List<BlogResponseDTO>>(200, "ok", service.list());
}
직접 해본 것:
결과:
#LGCNS #LGCNS6기 #개발자 #LGCNSINSPIRECAMP
잘 읽고 갑니다. 파이팅 하세요!