일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 자바의 정석 기초편 ch6
- 2024 정보처리기사 시나공 필기
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch14
- 자바 기본편 - 다형성
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch2
- 스프링 mvc2 - 타임리프
- jpa - 객체지향 쿼리 언어
- 2024 정보처리기사 수제비 실기
- 스프링 mvc1 - 스프링 mvc
- 자바 중급1편 - 날짜와 시간
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch9
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch5
- 스프링 db2 - 데이터 접근 기술
- 스프링 mvc2 - 검증
- 스프링 입문(무료)
- @Aspect
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch4
- 스프링 mvc1 - 서블릿
- 자바 중급2편 - 컬렉션 프레임워크
- 자바 고급2편 - io
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch7
- Today
- Total
나구리의 개발공부기록
I/O 활용, 회원 관리 예제(메모리, 파일에 보관, DataStream, ObjectStream), XML, JSON, 데이터베이스 본문
I/O 활용, 회원 관리 예제(메모리, 파일에 보관, DataStream, ObjectStream), XML, JSON, 데이터베이스
소소한나구리 2025. 2. 22. 15:49출처 : 인프런 - 김영한의 실전 자바 - 고급2편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
회원 관리 예제
메모리
I/O를 사용해서 회원 데이터를 관리하는 예제를 만들어 보기
요구사항
회원을 등록하고 등록한 회원의 목록을 조회할 수 있는 회원 관리 프로그램을 작성
회원의 속성은 다음과 같음
- ID
- Name
- Age
프로그램 작동 예시
1.회원 등록 | 2.회원 목록 조회 | 3.종료
선택: 1
ID 입력: id1
Name 입력: name1
Age 입력: 20
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3.종료
선택: 1
ID 입력: id2
Name 입력: name2
Age 입력: 30
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3.종료
선택: 2
회원 목록:
[ID: id1, Name: name1, Age: 20]
[ID: id2, Name: name2, Age: 30]
1.회원 등록 | 2.회원 목록 조회 | 3.종료
선택: 3
프로그램을 종료합니다.
회원 클래스
- id, name , age 필드
- 기본생성자와 각 필드의 값을 채우는 생성자
- Getter, Setter
- ToString 오버라이딩
package io.member;
public class Member {
private String id;
private String name;
private Integer age;
public Member() {
}
public Member(String id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Member{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
회원을 저장하고 관리하는 인터페이스
- add(): 회원 객체를 저장
- findAll(): 저장한 회원 객체를 List로 모두 조회
package io.member;
public interface MemberRepository {
void add(Member member);
List<Member> findAll();
}
회원을 저장하고 관리하는 구현체
- 간단하게 메모리에 회원을 저장하고 관리
- 회원을 저장하면 내부에 있는 members 리스트에 회원이 저장됨
- 회원을 조회하면 members 리스트가 반환됨
package io.member;
public class MemoryMemberRepository implements MemberRepository {
private final List<Member> members = new ArrayList<>();
@Override
public void add(Member member) {
members.add(member);
}
@Override
public List<Member> findAll() {
return members;
}
}
프로그램 main
- 콘솔을 통해 회원 등록, 목록 조회 기능을 제공함
package io.member;
public class MemberConsoleMain {
private static final MemberRepository repository = new MemoryMemberRepository();
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("1.회원 등록 | 2.회원 목록 조회 | 3. 종료");
System.out.print("선택: ");
int choice = scanner.nextInt();
scanner.nextLine(); // newLine 제거
switch (choice) {
case 1:
// 회원 등록
registerMember(scanner);
break;
case 2:
// 회원 목록 조회
displayMembers();
break;
case 3:
System.out.println("프로그램을 종료합니다.");
return;
default:
System.out.println("잘못된 선택입니다. 다시 입력하세요");
}
}
}
private static void registerMember(Scanner scanner) {
System.out.println("ID 입력: ");
String id = scanner.nextLine();
System.out.println("Name 입력: ");
String name = scanner.nextLine();
System.out.println("Age 입력: ");
int age = scanner.nextInt();
scanner.nextLine(); // newLine 제거
Member newMember = new Member(id, name, age);
repository.add(newMember);
System.out.println("회원이 성공적으로 등록되었습니다.");
}
private static void displayMembers() {
List<Member> members = repository.findAll();
System.out.println("회원 목록:");
for (Member member : members) {
System.out.printf("[ID: %s, Name: %s, Age: %d]\n", member.getId(), member.getName(), member.getAge());
}
}
}
/* 실행 결과
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 1
ID 입력:
1
Name 입력:
사람1
Age 입력:
20
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 1
ID 입력:
2
Name 입력:
사람2
Age 입력:
30
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 1
ID 입력:
3
Name 입력:
사람3
Age 입력:
42
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 2
회원 목록:
[ID: 1, Name: 사람1, Age: 20]
[ID: 2, Name: 사람2, Age: 30]
[ID: 3, Name: 사람3, Age: 42]
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 3
프로그램을 종료합니다.
*/
문제
이 프로그램은 잘 작동하지만, 데이터를 메모리에 보관하기 때문에 자바를 종료하면 모든 회원 정보가 사라짐
즉 프로그램을 다시 실행하면 모든 회원 데이터가 사라지기 때문에 프로그램을 영구 보존하려면 어딘가에 데이터를 저장해야 함
파일에 보관
회원 데이터를 영구 보존하려면 파일에 저장하면 됨
아래의 예시처럼 한 줄 단위로 회원 데이터를 파일에 저장
temp/members-txt.dat 예시
- 문자를 파일에 저장하는 것이므로 Reader, Writer를 사용하는 것이 편리함
- 한 줄 단위로 처리할 때는 BufferedReader가 유용하므로 BufferedReader, BufferedWriter를 사용하면 됨
id1,member1,20
id2,member2,30
FileMemberRepository
- MemberRepository 인터페이스가 잘 정의되어 있으므로 이 인터페이스를 기반으로 파일에 회원 데이터를 보관하는 구현체를 만들면 됨
- DELIMITER: 회원의 각 데이터를 구분하는 구분자를 , (쉼표)로 지정
package io.member;
public class FileMemberRepository implements MemberRepository {
private static final String FILE_PATH = "temp/members-txt.dat";
private static final String DELIMITER = ",";
@Override
public void add(Member member) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(FILE_PATH, UTF_8, true))) {
// append를 true로 하면 기존 파일을 지우지 않고 뒤에 추가 되는 데이터를 이어서 계속 저장함
bw.write(member.getId() + DELIMITER + member.getName() + DELIMITER + member.getAge());
bw.newLine(); // 개행 문자
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public List<Member> findAll() {
List<Member> members = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader(FILE_PATH, UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
String[] memberData = line.split(DELIMITER);
members.add(new Member(memberData[0], memberData[1], Integer.valueOf(memberData[2])));
}
return members;
} catch (FileNotFoundException e) {
return new ArrayList<>();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
회원 저장
- 회원 객체의 데이터를 읽어서 String 문자로 변경
- 여기서 write()는 String을 입력으로 받고 DELIMITER를 구분자로 사용함
- bw.write() 메서드의 인자에 Member의 아이디, 이름, 나이를 구분자인 , (쉼표)와 함께 String으로 변경하여 전달하면 bw.write("id,member1,20") 으로 저장할 문자가 전달됨
- 회원 데이터를 문자로 변경한 다음 파일에 보관하고 각 회원을 구분하기 위해 newLine()을 통해 다음 줄로 이동함
회원 조회
- line = br.readLine()
- 각 회원 하나하나를 불러오면 line = "id1,member1,20"처럼 하나의 회원 정보가 담긴 한 줄 문자가 입력됨
- String[] memberData = line.split(DELIMITER)
- line에 입력된 회원 데이터를 딜리미터로 분할하며 String 배열에 담아서 생성해 둔 members 리스트에 분할된 정보를 전달하여 새로운 회원 객체를 생성
- id, name은 String이기 때문에 타입이 같아서 그대로 전달할 수 있지만 age은 타입이 int이므로 숫자 타입으로 형변환을 해주어야 함
- FileNotFoundException e
- 회원 데이터가 하나도 없을 때는 temp/members-txt.dat 파일이 존재하지 않기 때문에 해당 예외가 발생하게 됨
- 이 경우 회원 데이터가 하나도 없는 것으로 보고 빈 리스트를 반환하도록 작성
** 참고: 빈 컬렉션 반환
- 빈 컬렉션을 반환할 때는 new ArrayList() 보다는 List.of()를 사용하는 것이 좋음
- 자세한 내용은 https://nagul2.tistory.com/422 참고
- 이번 예제에서도 List.of()를 사용하는 것이 좋으나 뒤에 나오는 ObjectStream 부분과 내용을 맞추기 위해 빈 컬렉션을 생성할 때 new ArrayList()를 사용하였음
try-with-resources
- 이 구문을 사용하면 자동으로 자원을 정리함
- try 코드 불록이 끝나면 자동으로 close()가 호출되면서 자원을 정리함
- 자세한 내용은 https://nagul2.tistory.com/412 참고
MemberConsoleMain
- MemoryMemberRepository 대신 FileMemberRepository를 사용하도록 코드를 수정하고 실행해 보면 MemberRepository 인터페이스를 사용한 덕분에 구현체가 변경되더라도 클라이언트의 다른 코드들을 변경하지 않아도 정상적으로 실행이 됨
- 파일에 회원 데이터를 저장한 덕분에 자바를 다시 실행해도 저장한 회원이 잘 조회되는 것을 확인할 수 있으며 처음에 회원 데이터가 없이 목록을 조회해도 에러가 발생하지 않고 빈 회원목록을 출력해 주는 것을 확인할 수 있음
package io.member;
public class MemberConsoleMain {
// private static final MemberRepository repository = new MemoryMemberRepository();
private static final MemberRepository repository = new FileMemberRepository();
// ... 기존 코드 동일 생략
}
/* 실행 결과
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 2
회원 목록:
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 1
ID 입력:
1
Name 입력:
등록1
Age 입력:
10
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 1
ID 입력:
2
Name 입력:
등록2
Age 입력:
20
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 2
회원 목록:
[ID: 1, Name: 등록1, Age: 10]
[ID: 2, Name: 등록2, Age: 20]
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 3
프로그램을 종료합니다.
다시 실행
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 2
회원 목록:
[ID: 1, Name: 등록1, Age: 10]
[ID: 2, Name: 등록2, Age: 20]
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 3
프로그램을 종료합니다.
*/
temp/member-txt.dat - 실행 결과
- 파일에 회원데이터가 잘 저장되어 있음
문제
- 회원 객체는 String, Integer 같은 자바의 다양한 타입을 사용하지만 지금은 이런 타입을 무시하고 모든 데이터를 문자로 변경해서 저장하는 부분이 아쉬움
- age의 경우 문자를 숫자로 변경하기 위한 코드를 따로 작성해야 함
- 또한 구분자(DELIMITER)를 사용하여 id, name, age 각 필드를 저장하고 조회할 때도 구분자를 사용해서 각 필드를 구분해야 하는 것이 번거로움
DataStream
DataMemberRepository
앞서 배운 예시 중 DataOutputStream, DataInputStream을 사용하면 자바의 데이터 타입을 그대로 사용할 수 있어 위에서 언급한 문제들을 해결할 수 있음
package io.member;
public class DataMemberRepository implements MemberRepository {
private static final String FILE_PATH = "temp/members-data.dat";
@Override
public void add(Member member) {
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(FILE_PATH, true))) {
dos.writeUTF(member.getId());
dos.writeUTF(member.getName());
dos.writeInt(member.getAge());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public List<Member> findAll() {
ArrayList<Member> members = new ArrayList<>();
try (DataInputStream dis = new DataInputStream(new FileInputStream(FILE_PATH))) {
members.add(new Member(dis.readUTF(), dis.readUTF(), dis.readInt()));
return members;
} catch (FileNotFoundException e) {
return new ArrayList<>();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
회원 저장
- 회원을 저장할 때는 회원 필드의 타입에 맞는 메서드를 호출하면 됨
- 이전 예제에서는 각 회원을 한 줄 단위로 구분하고 구분자와 함께 String으로 변환해야 했지만 여기서는 그런 구분도 필요 없고 타입에 맞게 저장할 수 있으므로 코드가 명확해지고 간결해짐
회원 조회
- 회원 데이터를 조회할 때도 구분자로 나누는 코드 없이 회원 필드의 각 타입에 맞는 메서드를 사용해서 조회하면 됨
- while문의 조건으로 dis.available() 메서드를 사용하여 불러올 스트림이 없을 때까지 반복해서 불러오도록 하면 모든 데이터가 members에 담겨서 출력됨
MemberConsoleMain
- DataMemberRepository를 사용하도록 코드를 수정하고 실행해 보면 파일도 정상 보관 되며 데이터를 정상적으로 조회하는 모습을 확인할 수 있음
- 타입 그대로 저장했기 때문에 파일에서는 당연히 일부 텍스트는 깨져 보임
/* 실행 결과
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 1
ID 입력:
사람1
Name 입력:
김사람
Age 입력:
15
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 1
ID 입력:
동물1
Name 입력:
이동물
Age 입력:
23
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 1
ID 입력:
사람1
Name 입력:
조사람
Age 입력:
20
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 2
회원 목록:
[ID: 사람1, Name: 김사람, Age: 15]
[ID: 동물1, Name: 이동물, Age: 23]
[ID: 사람1, Name: 조사람, Age: 20]
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 3
프로그램을 종료합니다.
다시 실행
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 2
회원 목록:
[ID: 사람1, Name: 김사람, Age: 15]
[ID: 동물1, Name: 이동물, Age: 23]
[ID: 사람1, Name: 조사람, Age: 20]
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 3
프로그램을 종료합니다.
*/
temp/member-data.dat - 실행 결과
- 자바 타입을 바로 저장했기 때문에 파일에서는 깨져서 보이지만 출력 시에는 정상적으로 보이는 것을 확인할 수 있음
DataStream 원리
DataStream이 구분자나 한 줄 라인 없이도 정확하게 데이터를 저장하고 조회할 수 있는 이유는 문자의 길이를 따로 저장하기 때문임
String 타입
readUTF()로 문자를 읽어올 때 UTF-8 형식으로 문자를 저장하는데, 이때 2byte를 추가로 사용해서 앞에 글자의 길이를 저장해 둠(65535 길이까지만 사용 가능)
dos.writeUTF("id1"); // 저장
dis.readUTF(); // 조회
위처럼 회원 데이터를 저장하고 조회한다고 가정하면 3id1(2byte(문자 길이) + 3byte(실제 문자 데이터))처럼 저장됨
"id1"를 UTF-8을 저장할 때 문자 앞에 글자의 길이를 저장해 두고 readUTF()로 읽어 들일 때 2byte로 글자의 길이를 먼저 확인한 후 해당 길이만큼 글자를 읽어 들임
이 경우에는 2byte를 사용해서 3이라는 문자의 길이를 숫자로 보관하고 나머지 3byte로 실제 문자 데이터를 보관함
기타 타입
- 자바의 Int(Integer)는 4byte를 사용하기 때문에 그대로 4byte를 사용해서 파일을 저장하고 읽을 때도 4byte를 읽어서 복원함
dos.writeInt(20); // 저장
dis.readInt(); // 조회
저장 예시
- 이해를 돕기 위해서 저장된 파일 예시를 각 필드를 엔터로 구분했지만 실제로는 엔터 없이 한 줄로 연결되어 있음
- 실제로 저장된 파일은 문자와 byte가 섞여 있음
dos.writeUTF("id1");
dos.writeUTF("name1");
dos.writeInt(20);
dos.writeUTF("id2");
dos.writeUTF("name2");
dos.writeInt(30);
/* 저장된 파일 예시
3id1(2byte(문자 길이) + 3byte)
5name1(2byte(문자 길이) + 5byte)
20(4byte)
3id2(2byte(문자 길이) + 3byte)
5name2(2byte(문자 길이) + 5byte)
30(4byte)
*/
정리
DataStream 덕분에 자바의 타입도 그대로 사용하고 구분자도 제거할 수 있게 되었음
추가로 모든 데이터를 문자로 저장할 때 보다 저장 용량도 더 최적할 수 있음
숫자의 1,000,000,000(10억)을 문자로 저장하게 되면 숫자 하나하나를 문자로 저장해야 하기 때문에 1byte로 표현하는 ASCII 인코딩을 적용해도 총 10byte가 사용됨
하지만 이것을 자바의 int처럼 4byte로 저장한다면 그냥 4byte만 사용하게 되므로 저장 용량 최적화 됨
물론 이렇게 byte를 직접 저장하면 문서 파일을 열어서 확인하고 수정하는 것이 어려운 단점이 있음
문제
DataStream으로 회원 데이터를 더 편리하게 저장할 수 있는 것은 맞지만 회원의 필드 하나하나를 다 조회해서 각 타입에 맞도록 저장해야 함
이것은 회원 객체를 저장한다기보다는 회원 데이터를 하나하나 분류해서 따로 저장한 것임
처음에 사용했던 MemoryMemberRepository에서 회원 객체를 자바 컬렉션에 저장했을 때와 비교해 보면 이 때는 회원 객체를 저장할 때 복잡하게 회원의 필드를 하나씩 꺼내서 따로 저장할 필요가 없이 단순하게 회원 객체를 그대로 자바 컬렉션에 보관하였고, 조회할 때도 마찬가지였음
자바는 이처럼 객체를 저장하는 것처럼 파일에 저장할 수 있는 방법을 제공함
ObjectStream
회원 인스턴스도 메모리 어딘가에 보관되어 있는데, 이렇게 메모리에 보관되어 있는 객체를 읽어서 파일에 저장하기만 하면 아주 간단하게 회원 인스턴스를 저장할 수 있음
ObjectStream을 사용하면 이렇게 메모리에 보관되어 있는 회원 인스턴스를 자바 컬렉션에 회원 객체를 보관하듯이 편리하게 저장할 수 있음
객체 직렬화
자바 객체 직렬화(Serialization)는 메모리에 있는 객체 인스턴스를 바이트 스트림으로 변환하여 파일에 저장하거나 네트워크를 통해 전송할 수 있도록 하는 기능임
이 과정에서 객체의 상태를 유지하여 나중에 역직렬화(Deserialization)를 통해 원래의 객체로 복원할 수 있음
Serializable 인터페이스
- 이 인터페이스에는 아무런 기능이 없이 단지 직렬화 가능한 클래스라는 것을 표시하기 위한 인터페이스임
- 메서드 없이 단지 표시가 목적인 인터페이스를 마커 인터페이스라고 함
package java.io;
public interface Serializable {
}
Member - Serializable 추가
- Member클래스에 Serializable을 구현하여 직렬화될 수 있도록 함
- 만약 해당 인터페이스가 없는 객체를 직렬화하면 NotSerializableException 예외가 발생함
public class Member implements Serializable {
private String id;
private String name;
private Integer age;
// ... 기존 코드 동일 생략
}
ObjectMemberRepository
- ObjectOutputStream과 ObjectInputStream을 사용하여 매우 간단한 코드로 회원 데이터를 저장하고 조회하는 코드를 완성시켰음
- 저장
- findAll()로 조회 후 회원을 추가해서 저장하는 이유는 기존 파일에 회원을 하나씩 추가하면서 저장할 수 있는 것이 아니라 컬렉션을 통째로 파일로 만들기 때문에 이전 데이터를 컬렉션으로 조회해서 추가할 데이터를 추가한 뒤에 다시 저장하는 방식임
- ObjectOutputStream에서는 조회할 때 FileNotFoundException이 발생했을 때 new ArrayList()로 빈 배열을 반환해야만 하는 이유가 저장하는 방식 때문임
- FileStream, DataStream에서는 빈 배열을 반환할 때 List.of()를 권장했지만 여기에서는 lIst.of()를 사용하여 불변인 빈 리스트 생성하면 변경할 수 없는 컬렉션이기 때문에 데이터를 저장할 수 없게 됨
- 조회
- 반환타입이 Object 타입이므로 회원을 그대로 반환하기 위해 List <Member>로 형변환 해서 반환함
package io.member;
public class ObjectMemberRepository implements MemberRepository {
private static final String FILE_PATH = "temp/members-obj.dat";
@Override
public void add(Member member) {
List<Member> members = findAll();
members.add(member);
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH))) {
oos.writeObject(members);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public List<Member> findAll() {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH))) {
Object findObject = ois.readObject();
return (List<Member>) findObject;
} catch (FileNotFoundException e) {
return new ArrayList<>();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
MemberConsoleMain
ObjectMemberRepository를 사용하도록 변경하고 실행해 보면 정상적으로 모두 동작하는 것을 확인할 수 있음
/* 실행 결과
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 2
회원 목록:
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 1
ID 입력:
사람1
Name 입력:
김사람
Age 입력:
20
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 2
회원 목록:
[ID: 사람1, Name: 김사람, Age: 20]
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 1
ID 입력:
동물1
Name 입력:
김동물
Age 입력:
20
회원이 성공적으로 등록되었습니다.
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 2
회원 목록:
[ID: 사람1, Name: 김사람, Age: 20]
[ID: 동물1, Name: 김동물, Age: 20]
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 3
프로그램을 종료합니다.
다시 실행
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 2
회원 목록:
[ID: 사람1, Name: 김사람, Age: 20]
[ID: 동물1, Name: 김동물, Age: 20]
1.회원 등록 | 2.회원 목록 조회 | 3. 종료
선택: 3
프로그램을 종료합니다.
*/
temp/members-obj.dat 실행 결과
- 문자와 byte가 섞여 있고 ArrayList, Member 같은 클래스 정보도 함께 저장됨
직렬화
- ObjectOutputStream를 사용하면 객체 인스턴스를 직렬화해서 byte로 변경할 수 있음
- 회원 객체 하나가 아니라 회원 목록 전체를 파일에 저장해야 하기 때문에 members 컬렉션을 직렬화해야 함
- 만약 기존에 파일에 저장된 데이터에 추가하고 싶다면 기존 파일을 불러와서 추가할 데이터를 members에 추가한 후 직렬화 해야 함
- oos.writeObject(members)를 호출하면 members 컬렉션과 그 안에 포함된 Member를 모두 직렬화해서 byte로 변경하고 oos와 연결되어 있는 FileOutputStream이 결과를 출력(저장)함
- 참고로 ArrayList도 Serializable 인터페이스를 구현하고 있어서 직렬화할 수 있음
역직렬화
- ObjectInputStream을 사용하면 byte를 역직렬화해서 객체 인스턴스로 만들 수 있음
- Object findObject = ois.readObject()를 사용하면 역직렬화가 되며 반환 타입이 Object이므로 캐스팅해서 사용해야 함
정리
객체 직렬화 덕분에 객체를 매우 편리하게 저장하고 불러올 수 있었음
객체를 바이트로 변환할 수 있어 모든 종류의 스트림에 전달할 수 있으므로 파일에 저장하는 물론 네트워크를 통해 객체를 전송하는 것도 가능하게 함
이러한 특성 때문에 초기에는 분산 시스템에서 활용되었음
그러나 객체 직렬화는 1990년대 등장한 기술로 초창기에는 인기가 있었지만 시간이 지나면서 여러 단점이 드러났고 대안 기술이 등장하면서 점점 그 사용이 줄어들게 되어 현재는 객체 직렬화를 거의 사용하지 않음
객체 직렬화 관련해서는 객체 직렬화 버전을 관리하는 serialVersionUID나, 특정 필드를 직렬화하지 않도록 무시하는 transient 키워드 등 더 학습할 내용이 존재함
그러나 현대에서는 객체 직렬화를 잘 사용하지 않기 때문에 이런 것이 있다는 정도만 알고 넘어가면 됨
XML, JSON, 데이터베이스
객체 직렬화의 한계
객체 직렬화를 사용하지 않는 이유
- 버전 관리의 어려움
- 클래스 구조가 변경되면 이전에 직렬화된 객체와의 호환성 문제가 발생함
- serialVersionUID 관리가 복잡함
- 플랫폼 종속성
- 자바 직렬화는 자바 플랫폼에 종속적이어서 다른 언어나 다른 시스템과의 상호 운용성이 떨어짐
- 성능 이슈
- 직렬화/역직렬화 과정이 상대적으로 느리고 리소스를 많이 사용함
- 유연성 부족
- 직렬화된 형식을 커스터마이즈 하기 어려움
- 크기 효율성
- 직렬화된 데이터의 크기가 상대적으로 큼
객체 직렬화의 대안1 - XML
<member>
<id>id1</id>
<name>name1</name>
<age>20</age>
</member>
플랫폼 종속성 문제를 해결하기 위해 2000년대 초반에 XML이라는 기술이 인기를 끌었음
XML은 매우 유연하고 강력했지만 복잡성과 무거움이라는 문제가 있었고 태그를 포함한 XML 문서의 크기가 커서 네트워크 전송 비용도 증가했음
객체 직렬화의 대안2 - JSON
{ "member": { "id": "id1", "name": "name1", "age": 20 } }
JSON은 가볍고 간결하며 자바스크립트와의 자연스러운 호환성 덕분에 웹 개발자들 사이에서 빠르게 확산되었음
2000년대 후반, 웹 API와 RESTful 서비스가 대중화되면서 JSON은 표준 데이터 교환 포맷으로 자리 잡았음
XML은 데이터 구조의 복잡성과 엄격한 스키마 정의가 필요한 초기 웹 서비스와 엔터프라이즈 환경에서 중요한 역할을 했지만 시간이 지나면서 JSON과 같은 가볍고 효율적인 데이터 형식이 더 많이 채택됨
JSON은 웹과 모바일 애플리케이션의 발전과 함께 급속히 인기를 얻었으며 현재는 대부분의 데이터 교환에서 기본적인 포맷으로 사용되고 있음
XML은 특정 영역에서 여전히 사용되지만 JSON이 현대 소프트웨어 개발의 주류로 자리 잡았음
지금은 웹 환경에서 데이터를 교환할 때 JSON이 사실상 표준임
객체 직렬화의 대안3 - Protobuf, Avro(더 적은 용량, 더 빠른 성능)
- JSON은 거의 모든 곳에서 호환이 가능하고 사람이 읽고 쓰기 쉬운 텍스트 기반 포맷이어서 디버깅과 개발이 쉬움
- 만약 매우 작은 용량으로 더 빠른 속도가 필요하다면 protobuf, Avro 같은 대안 기술이 있음
- 이런 기술은 호환성은 떨어지지만 byte 기반에 용량과 성능 최적화가 되어있으므로 매우 빠르지만 byte 기반이기 때문에 JSON처럼 사람이 직접 읽기는 어려움
정리
- 자바 객체 직렬화는 대부분 사용하지 않음
- JSON이 사실상 표준이므로 JSON을 먼저 고려해야 함
- 대부분 JSON만 사용해도 충분하지만 만약 성능 최적화가 매우 중요하다면 Protobuf, Avro 같은 기술을 고려하면 됨
데이터베이스
앞서 설명한 것처럼 회원 객체 같은 구조화된 데이터를 주고받을 때는 JSON 형식을 주로 사용하지만 어떤 형식이든 데이터를 저장할 때 파일에 데이터를 직접 저장하는 방식은 몇 가지 큰 한계가 있음
- 데이터의 무결성을 보장하기 어려움, 여러 사용자가 동시에 파일을 수정하거나 접근하려고 할 때 데이터의 충돌이나 손상 가능성이 높아져 데이터의 일관성을 유지하는 것이 매우 어려움
- 데이터의 검색과 관리가 비효율적임, 파일에 저장된 데이터는 특정 형식 없이 단순히 저장될 수 있기 때문에 필요한 데이터를 빠르게 찾는 데 많은 시간이 소요될 수 있음, 특히 데이터의 양이 방대해질수록 검색 속도는 급격히 저하됨
- 보안 문제, 파일 기반 시스템에서는 민감한 데이터를 안전하게 보호하기 위한 접근 제어와 암호화 등이 충분히 구현되지 않을 수 있음, 결과적으로 데이터 유출이나 무단 접근의 위험이 커질 수 있음
- 대규모 데이터의 효율적인 백업과 복구가 필요함
이러한 문제점들을 하나하나 해결하면서 발전한 서버 프로그램이 바로 데이터베이스이며 대부분의 현대 애플리케이션에서는 데이터베이스를 사용함
데이터베이스는 위 한계들을 모두 극복하고 대량의 데이터를 효율적으로 저장, 관리, 검색할 수 있는 강력한 도구를 제공함
이런 이유로 실무에서는 대부분의 데이터를 파일에 저장하지 않고 데이터베이스에 저장하며 이미지, 영상처럼 큰 데이터만 파일로 보관함
데이터베이스만 해도 하나의 큰 분야이기 때문에 학습할 것이 매우 많으며 백엔드 개발자가 목표라면 데이터베이스는 반드시 깊이 있게 학습해야 함