이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.
객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다 - 조영호(객체지향의 사실과 오해)
이번 글에서는 JPA에서 외래 키
로 조인을 사용하고, 객체
는 참조를 사용해서 연관관계를 매핑하는 방법에 대해서 알아보자.
예를 들어, 회원과 팀이 있고,
회원은 하나의 팀에만 소속될 수 있고,
회원(다)과 팀(일)은 다대일 관계라고 시나리오를 가정해보자.
이때, 객체를 테이블에 맞추어서 모델링을 하면, Member, Team 두개의 테이블을 통해 식별자(member.getId(), team.getId())로 조회해야 한다.
이처럼 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
따라서 객체지향 설계로 협력 관계를 만들기 위해 아래와 같이 생각해서 매핑한다.
테이블은 외래 키
로 조인을 사용해서 연관된 테이블을 찾는다.
객체는 참조
를 사용해서 연관된 객체를 찾는다.
객체 지향적인 모델링(객체 연관관계 사용)을 하면 아래 그림과 같다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
// @Column(name ="TEAM_ID")
// private Long teamId;
@ManyToOne // User: M이므로 '다'에 속하기 때문에 Many~로 시작한다.
@JoinColumn(name = "TEAM_ID") // 조인할 테이블의 식별키를 작성한다. => 외래 키
private Team team;
// 아래 Getter 생략
}
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장
em.persist(member);
//조회
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@ManyToOne // Member: M이므로 '다'에 속하기 때문에 Many~로 시작한다.
@JoinColumn(name = "TEAM_ID") // 조인할 테이블의 식별키를 작성한다. => 외래 키
private Team team;
// 아래 부분 생략
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team") // Team은 일대다에 '일'에 속하므로 @One으로 시작한다.
List<Member> members = new ArrayList<Member>();
// 아래 부분 생략
}
(mappedBy = "team")
은 Member
엔티티에 있는 Team의 변수명인 team과 연관되었다는 걸 의미한다.객체와 테이블 간에 연관관계를 맺는 차이를 이해해보자.
객체
는 연관관계가 2개이고, 테이블
은 연관관계가 1개로 구성되어 있다.
객체) 회원 -> 팀 연관관계 1개(단방향), 팀 -> 회원 연관관계 1개(단방향)
테이블) 회원 <-> 팀의 연관관계 1개(양방향)
객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개
다.
따라서 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
이때, 둘 중 하나로 외래 키를 관리해야 한다.
양방향 매핑에는 다음과 같은 규칙이 있다.
객체의 두 관계중 하나를 연관관계의 주인
으로 지정해야 한다.
연관관계의 주인만이 외래 키
를 관리(등록, 수정)할 수 있다.
주인
은 mappedBy 속성을 사용하지 않는다.
주인이 아닌쪽
은 읽기만 가능하므로 mappedBy 속성
으로 주인을 지정해준다.
결론부터 말하면, 외래 키
의 위치를 기준으로 연관관계 주인을 정하면 된다. (연관관계 주인 => 외래 키가 있는 곳)
여기서는 Member.team
이 연관관계의 주인이다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
team.getMembers().add(member);
//연관관계의 주인에 값 설정
member.setTeam(team); //**중요**
em.persist(member);
단방향 매핑만으로도 이미 연관관계 매핑은 완료된다.
양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
JPQL에서 역방향으로 탐색할 일이 많을 때 사용한다.
정리하면, [1] 처음에는 단방향 매핑으로 잡은 뒤, [2] 필요할 때 양방향으로 추가해준다. (단방향 -> 양방향으로 바꿀 때 테이블에 영향을 주지 않는다.)
이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.
@Entity
가 붙은 클래스는 JPA가 관리하며, 엔티티
라 부른다.
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity
가 필수이다.
몇 가지 주의점이 있다.
기본 생성자가 필수이다. (파라미터가 없는 public 또는 protected 생성자)
final 클래스, enum 클래스, interface, inner 클래스는 사용할 수 없다.
저장할 필드에 final 사용할 수 없다.
@Entity 속성 정리
속성: name
: JPA에서 사용할 엔티티 이름을 지정한다.
기본값: 클래스 이름을 그대로 사용한다.
@Entity(name = "User")
같은 클래스 이름이 없으면 가급적 기본값을 사용한다.
@Table
은 엔티티와 매핑할 테이블을 지정한다.
속성 1) name
: 클래스 이름이 MySQL 예약어와 겹치면 매핑되는 테이블 이름은 복수형
으로 지정한다.
기본값은 엔티티 이름을 사용한다.
ex) Entity Class 이름 = User -> @Table(name = "users")
속성 2) catalog
: 데이터베이스 catalog를 매핑한다.
속성 3) schema
: 데이터베이스 schema를 매핑한다.
속성 4) uniqueConstraints
: DDL 생성 시에 유니크 제약 조건을 생성한다.
User 클래스(엔티티)
package woorifisa.goodfriends.backend.user.domain;
import woorifisa.goodfriends.backend.global.common.BaseTimeEntity;
import woorifisa.goodfriends.backend.user.exception.InvalidUserException;
import javax.persistence.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Table(name = "users")
@Entity
public class User extends BaseTimeEntity {
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-z0-9._-]+@[a-z]+[.]+[a-z]{2,3}$");
private static final int MAX_NICKNAME_LENGTH = 20;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(name = "email", nullable = false)
private String email;
@Column(name = "nickname", nullable = false)
private String nickname;
@Column(name = "profile_image_url", nullable = false)
private String profileImageUrl;
protected User() {
}
public User(String email, String nickname, String profileImageUrl) {
validateEmail(email);
validateNickname(nickname);
this.email = email;
this.nickname = nickname;
this.profileImageUrl = profileImageUrl;
}
private void validateEmail(final String email) {
Matcher matcher = EMAIL_PATTERN.matcher(email);
if (!matcher.matches()) {
throw new InvalidUserException();
}
}
private void validateNickname(final String nickname) {
if(nickname.isEmpty() || nickname.length() > MAX_NICKNAME_LENGTH) {
throw new InvalidUserException(String.format("이름은 1자 이상 %d자 이하여야 합니다.", MAX_NICKNAME_LENGTH));
}
}
// getter
}
DDL이란 데이터베이스를 정의하는 언어이며, 데이터를 생성, 수정, 삭제하는 등의 데이터 전체의 골격을 결정하는 역할을 하는 언어이다.
테이블 관련 명령어 : create(생성), alter(수정), drop(삭제), truncate(초기화)
DDL을 애플리케이션 실행 시점에 자동으로 생성해준다. (테이블 -> 객체 중심)
데이터베이스 방언
(DB Dialect) 을 활용해서 데이터베이스에 적절한 DDL을 생성한다.
Hibernate의 경우엔 persistence.xml
에서 hibernate.dialect 설정 값을 변경하면 된다.
예를 들어, H2 Dialect 설정 코드는 <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
이다.
```markdown
SQL은 다음과 같이 표준 SQL인 ANSI SQL이 있으며, 이외에 각 DBMS Vendor(벤더, 공급업체)인 MS-SQL, Oracle, MySQL, PostgreSQL 에서 자신만의 기능을 추가한 SQL이 있다.
이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.
웹 애플리케이션을 개발하면서 고객으로부터 요청이 오면, 엔티티 매니저 팩토리를 통해 엔티티 매니저를 생성한다.
엔티티 매니저는 데이터베이스 커넥션 풀을 사용해서 DB를 사용하게 된다.
영속성 컨텍스트
란 엔티티를 영구 저장하는 환경이라는 뜻을 가진 논리적인 개념으로 어플리케이션과 DB 사이에서 객체를 보관하는 가상의 DB같은 역할을 한다.
즉, 애플리케이션에서 DB에 저장하기 전에 사용을 하는 임시 저장 공간이라고 이해를 하면 편할 것이다.
EntityManager.persist(entity);
엔티티 매니저를 통해서 영속성 컨텍스트
에 접근한다.
엔티티 매니저
(Entity Manager)란 영속성 컨텍스트에 접근하여 엔티티에 대한 데이터베이스 작업을 제공하는 것을 의미한다.
엔티티 매니저
(Entity Manager)는 엔티티를 저장하고, 수정하고, 삭제하고 조회하는 등 엔티티와 관련된 모든 일을 처리한다.
영속성 컨텍스트를 통해 데이터의 상태 변화를 감지하고 필요한 쿼리를 자동으로 수행한다.
비영속
상태라고 한다.영속
상태라고 한다.import javax.persistence.Persistence;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin()
try {
// 비영속
Member member = new Member();
member.setId(100L);
member.setName("HelloJPA");
// 영속
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
ex.close();
}
}
}
Member 엔티티를 영속성 컨텍스트에서 분리한 상태를 준영속
상태라고 한다.
준영속 상태로 만드는 방법
em.detach(entity) : 특정 엔티티만 준영속 상태로 전환
em.clear() : 영속성 컨텍스트를 완전히 초기화
em.close() : 영속성 컨텍스트를 종료
객체를 삭제한 상태를 삭제
라고 하며, 코드에서는 em.remove(member);
를 의미한다.
//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, “memberA");
em.remove(memberA); //엔티티 삭제
영속성 컨텍스트 내부에는 엔티티 인스턴스를 캐싱하는 1차 캐시
가 있다.
일반적으로 트랜잭션 내에서 유효한 생명주기를 갖는다.
member1
를 조회하면, JPA는 DB가 아닌 1차 캐시에 저장된 값을 가져온다.그런데 만약 member2
를 조회할 때, 1차 캐시에 없다면, DB를 조회한다. 그런 다음에 해당 값을 1차 캐시에 저장하고 값을 반환한다.
사실, 엔티티 매니저라는 건 데이터베이스 트랜잭션이 끝날 때 종료된다. ⇒ 비즈니스가 끝나면, 해당 엔티티 매니저가 사라져버린다.
차 캐시라는건 데이터베이스 하나의 트랜잭션 안에서만 효과가 있기 때문에 성능에 대한 장점은 없다.
영속성 컨텍스트는 영속성 컨텍스트 안에서 영속 엔티티들의 동일성을 보장한다.
따라서 같은 식별자를 통해 1차 캐시에서 얻은 엔티티 인스턴스는 ==
연산을 통해 동일성을 보장한다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); //동일성 비교 true
JPA가 영속 엔티티의 동일성을 보장한다. 마치 자바 컬렉션에서 똑같은 레퍼런스 객체를 꺼내오는 것과 같다. 1차 캐시가 있기 때문에 가능한 것이다.
같은 트랜잭션 안에서 비교할 때에만 동일하다는 것이다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA); // 1
em.persist(memberB); // 2
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // 3 [트랜잭션] 커밋
em.persist(memberA);
[1] em.persist(memberA);
을 통해 memberA가 1차 캐시에 들어간다.(집어 넣는다)
[2] 그러면서 동시에 JPA가 이 엔티티를 분석해서 INSERT SQL 쿼리를 생성한다.
[3] 그래서 쓰기 지연 SQL 저장소에 쌓아둔다.
em.persist(memberB);
[4] 그런 다음에 em.persist(memberB);
을 하면, memberB도 1차 캐시에 들어간다.(집어 넣는다)
[5] 그리고 이때, INSERT SQL 쿼리를 생성해서 쓰기 지연 SQL 저장소에 차곡차곡 쌓아둔다.
transaction.commit();
[6] 그리고 transaction.commit();
을 통해 커밋하는 순간 쓰기 지연 SQL 저장소
에 있던 데이터들이 flush가 되면서, 데이터베이스
에 INSERT SQL을 보낸다.
[7] 그리고 실제 데이터베이스가 commit 된다.
변경 감지
란 JPA를 사용하여 데이터를 수정하려면 Entity
를 조회하여 조회된 Entity
데이터를 변경만 하면 데이터베이스
에 자동으로 반영이 되도록 하는 기능이다.[1] 데이터베이스에 커밋하면, 내부적으로 flush가 발생하고
[2] JPA가 엔티티하고 스냅샷을 비교한다.
엔티티 인스턴스 값
을 읽어온 그 시점의 최초 상태를 영속성 컨텍스트
에 저장하는 데, 이를 스냅샷
이라 부른다.만약 MemberA에 대한 데이터가 변경되면 쓰기 지연 SQL 저장소
에 [3] UPDATE SQL 쿼리를 생성한다.
그리고 [4] UPDATE SQL 쿼리를 flush를 통해 데이터베이스에 반영하고
[5] commit 하게 된다.
flush
란 영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는 것을 말한다.
플러시가 발생하면, 다음 3가지가 일어난다.
변경 감지
수정된 엔티티의 쓰기 지연 SQL 저장소에 등록
쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시하는 방법은 3가지가 있다.
em.flush() - 직접 호출 -> 쿼리가 DB에 반영되어 버린다.
트랜잭션 커밋 - 플러시 자동 호출
JPQL 쿼리 실행 - 플러시 자동 호출
플러시 모드 옵션은 기본적으로 AUTO(em.setFlushMode(FlushModeType.AUTO
)로 사용된다. (거의 사용할 일은 없다)
FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러스 (기본값)
FlushModeType.COMMIT : 커밋할 때만 플러시
정리하면, 플러시는
영속성 컨텍스트를 비우지 않고, 변경 내용을 데이터베이스에 동기화하고
트랜잭션 커밋 직전에만 동기화하면 된다.
이 글은 함께 자라기 책을 읽고 저의 생각과 같이 정리한 내용입니다.
책을 2월에 사고 난 뒤 9개월이 지난 지금에서야 다 읽게 되었다.
2월 당시에는 팀 프로젝트를 하기 전이라, 협업
에 대해 크게 흥미가 없어서 그런지 집중이 잘 안됐다.
하지만 3월부터 10월까지 진행했던 히빗과 8월부터 9월까지 진행했던 굿프렌즈 팀 프로젝트를 경험한 이후에, 이 책을 다시 읽어보자는 생각이 들었다.
그래서 기존에 중반까지 읽었던 내용을 주말 동안 다시 복습 겸? 읽으면서 완독을 했다.
확실히 협업과 관련된 경험을 하고 나서 읽으니까, 공감된 부분들도 많았고 더 관심 있게 읽으면서 빠르게 완독할 수 있었다.
아래는 책을 읽으면서 개인적으로 느낀 인상 깊었던 부분 몇 개를 내 생각과 같이 정리해 보려고 한다.
“소프트웨어 개발에서 개발에서 점차 경력 연수를 중시하는 문화가 사라질 것이다. 따라서 개발자들은 자신의 경력 연차 외에 다른 것에도 신경을 써야 한다.”
몇 년 전까지만 해도 안데쉬 에릭손의 1만 시간 법칙
이 유행이었다. 그런데 이 법칙에서 나오는 시간을 본인의 경력
과 연관 지어선 안된다는 의미이다.
예를 들어, 일상생활에 우리는 어렸을 때부터 하루 세 번 3분씩 이를 닦는다. 하지만 이를 매일 닦는다고 10년, 20년 이후에도 이를 닦는 경력이 실력과 관련이 없다는 것을 누구나 알 수 있다.
여기서 말하는 의도적 수련
(deliberate practice)이란 자신의 기량을 향상시킬 목적으로 반복적으로 하는 수련을 의미한다.
국내 여러 스포츠 종목에서 국가대표로 지내는 선수분들은 세계 대회에 출전해서 메달을 따기 위해 매일 본인의 약점을 개선하고, 강점을 더 강화하는 애쓰는 수련을 하고 있다.
이 글을 읽고, ‘내가 공부한 것이 의도적인 수련인가?’라는 생각을 하게 되었다. 단순히 남이 작성한 글을 읽거나 강의를 듣는 시간이 아니라, 누군가한테 설명할 정도의 수준까지 내 것으로 만드는 시간이 의도적인 수련
이라고 생각한다.
정리하자면, 단순히 인풋만 하는 게 아니라 아웃풋까지 이어 나가는 과정과 결과가 중요하다고 생각한다.
유튜브 채널: 개발바닥 - 용기를 잃지않는 개발자 이력서 (55초 ~)
위의 내용과 완전히 일치하진 않지만, 최근 개발바닥 채널에서 “용기를 잃지않는 개발자 이력서”에서도 향로님이 말씀해주신 내용을 가져와 봤다.
나는 ~~을 위해 이걸 했고, 그래서 그 결과가 ~~이다 이어져야 개인의 실력이 성장하지 않을까 생각한다. 흔히 이력서를 작성할 때에도 Why - What - How - Result
과 같이 작성해야 보는 사람에게도 더 읽기 쉬워진다.
IT 기업에서 종사하시는 여러 직무의 분들이 연말에 한 해를 되돌아보면서 회고라는 글을 작성한다.
일 년 회고를 할 때 항상 되짚어 보는 것 중 하나가 나 자신에게 얼마나 투자를 했냐라는 것이다.
자기개발이 중요한 이유는 현재 나에게 무엇을 투자했느냐가 1년, 혹은 2년 후의 나를 결정한다고 느끼기 때문이다.
2022년 2학기 때 대학교에서 ‘운영체제’라는 과목을 수업하시는 교수님이 하신 말씀이 기억이 문득 생각났다.
교수님이 이전 삼성전자에 재직할 당시에, ‘전체 사원들 중 10% 이하 만이 자기개발을 하고, 나머지 90%는 현재 삶에 만족하는 경우가 많다’ 라고 하셨다.
이 책에서도 2시간 이상 자기개발에 투자하는 사람이 13.2%밖에 되지 않았다. (물론 2012년에 조사한 자료라, 현재와 다를 수 있다)
평소 자기개발을 열심히 하시면서, 블로그를 운영하시는 여러 현직자분들이 좋은 회사에 이직한 것을 블로그를 통해 많이 봤다.
나는 회사에서 일을 해본 경험이 적고, 이직 경험도 없어서 크게 와닿지는 않지만, 꾸준히 자기개발을 하면서 그러한 경험을 맛보는 기회를 언젠간 찾아오지 않을까 기대해 본다.
위 그림은 실력
과 작업 난이도
를 기준으로 심리 상태를 그린 도식이다.
주목해야 할 부분은 몰입
에 해당하는 영역인데, 이때 최고 수준의 집중력을 보이고, 그 덕분에 퍼포먼스나 학습 능력이 최대치가 될 수 있다고 한다.
해당 영역이 아닌, 실력이 작업 난이도를 초과하면 지루함을 느끼는 영역이나 실력보다 높은 난이도의 작업을 하면 불안함(혹은 두려움)을 느낀다고 한다.
‘팬시님! 현 프로젝트에 통합 및 인수 테스트를 작성해 주세요~ ‘와 같은 작업이 왔을 때 어렵다고 느껴지면, 우선 단위 테스트 작성하는 방법부터 익히도록 하자.
실수 관리 문화
에서는 실수가 나쁜 결과를 내기 전에 빨리 회복하도록 돕고, 실수를 공개하고, 실수에 대해 서로 이야기하고 거기에서 배우는 분위기가 생긴다.
회사 문화가 실수 관리에 가까울수록 그 기업의 혁신 정도가 더 높고, 회사의 수익성이 더 높다는 연구 자료가 있다.
즉, 실수 관리를 하는 문화일수록 학습
을 더 잘한다.
팀 프로젝트를 하다 보면 반드시는 아니지만, 실수가 종종 나타나는 경우가 있다. 그럴 때마다 실수를 한 팀원에게 따끔한 충고보단 따뜻한 조언을 하는 게 중요하다고 생각한다.
똑같은 실수에 대해 반복적이면 안 되겠지만, 처음 하는 실수에 대해서는 누구에게나 생길 수 있고, 그런 상황이 발생할 때 어떻게 해야 나은 방향으로 개선할 수 있을지 스스로에게 질문을 하면서 답을 찾아가 보자.
새로운 기술을 개인 또는 외부 스터디 활동을 통해서 배웠다고 회사에 바로 적용하기가 쉽지 않다.
아무리 기술적인 실천법이라고 해도 그 기술은 사회적 맥락속에서 실천되어야 하며, 그 기술의 성공을 위해서는 사회적 자본과 사회적 기술이 함께 필요하다.
이 책에서는 신뢰 구축을 보다 잘하는 사람을 사회적 자본이 좋은 사람들이라고 합니다. 그리고 사회적 자본이 좋은 사람들이 통상 사회적 기술이 뛰어나다고 합니다.
뛰어난 개발자일수록 동료와의 협업에 시간을 투자하는 부분으로 커뮤니케이션의 중요성을 강조합니다.
TDD(테스트 주도 개발)이라는 프로그래밍 기법을 가져왔다면, 해당 기법 전체를 단번에 적용하는 것보다는 팀원들과 지속적으로 협업하면서 기초적인 개념부터 천천히 도입하는 방향으로 가는게 좋다고 생각한다.
실제로 이전 팀 프로젝트에서 DDD 패턴을 도입했을 때, 해당 개념의 전체적인 것을 적용하지 않고 일부의 개념만 도입하고 이후에 추가하는 식으로 진행했었다.
신뢰 자산이 높은 조직은 커뮤니케이션 효율이나 생산성이 높다.
특히 애자일을 제대로 하려는 조직이라면 이 부분에 많은 노력을 들이고 있다.
이 책에서는 신뢰를 쌓는 데에 널리 사용되는 한 가지 방법은 투명성, 공유, 인터랙션이라고 한다.
자신이 한 작업물을 투명하게 서로 공유하고 그에 대해 피드백을 주고받으며 인터랙션을 하는 것이다.
개발자끼리 팀 프로젝트를 진행했을 때, 기획부터 같이 진행한 적이 있었다. 그때 각자만의 아이디어를 공유할 때 한 사람당 여러 개의 아이디어를 공유했는데, 여기서도 그와 관련된 글이 있다.
나의 아이디어가 1개밖에 없다면, 아아디어=나
가 되어서 상대방이 나에게 뭐라고 하기가 쉽지 않다.
반면에 복수로 공유하면, 그런 불안감이 상대적으로 덜하고, 부정적인 피드백을 수용하려는 마음도 있다.
이 책에서는 복수 공유를 한 그룹이 단수로 공유하는 것보다(개인당 아이디어가 1개) 신뢰도가 높아지고 성과도 더 좋았다고 합니다.
품질 전문가 제럴드 와인버그는 품질을 다음과 같이 정의했습니다.
“품질 이란 누군가에게 가치가 되는 것이다.”
사실 품질은 사람을 빼놓고 이야기할 수가 없다.
고품질을 얻으려고 노력하는 사람들은 ‘인간’에 대한 이해가 필수적이다.
설득
도 마찬가지이다. 대부분의 사람들이 설득을 하기 위해 객관성이 필요하다고 하는데, 사실 객관성을 정의하는 것도 사람이기 때문에, 이 개념 자체가 매우 주관적이라는 것이다.
회사에는 직원과 사장이 있고, 중요한 부분을 결정하고 실행하기 전에는 직원이 사장으로부터 승인을 받아야 하는 경우가 있다.
사장도 사람이기 때문에, 사장을 설득하라면 어쩔 수 없이 객관적인 자료뿐 아니라 감정적이고 직관적인 부분도 필요하다.
이 책에서는 남을 설득하려면 논리성과 객관성에 대한 환성을 버려야 하고, 그래야 현실적으로 설득이 가능하다고 한다.
결국, 설득에 성공하려면 우선 그 사람을 이해하는 것에서 출발해야 한다. 그런 이유로 설득을 하기 위해 ‘객관적’인 자료뿐 아니라 그 이상으로 상대를 이해하는데 많은 시간을 투자해야 한다.
상대방에게 신뢰감을 잃어버리면, 아무리 좋은 ‘객관적’인 자료와 근거가 있더라도 소용없다.
이 책에서는 심리적 안전감
을 내 생각이나 의견, 질문, 걱정, 혹은 실수가 드러났을 때 처벌받거나 놀리지 않을 거라는 믿음을 말한다고 한다.
이 개념은 정말 중요하다고 생각한다. 어떤 프로젝트를 진행할 때, 팀원 간의 의견을 주고 받는게 없다면, 이러한 심리적 안전감이 떨어질 것이고, 갈수록 상대방에 대한 이해가 낮아질 것이라고 본다.
히빗 프로젝트를 진행하면서, 매주 회의를 팀원들과 같이 진행하면서 모르는 부분이나 궁금한 부분들을 적극적으로 물어봤고, 나에게 질문이 들어오는 경우에도 최대한 자세히 알려주려고 노력했다.
개발을 하면서, 어려운 부분을 마주쳤을 때 혼자서 끙끙 앓는 것보다는 혼자서 어느 정도의 시간을 투자한 뒤에 동일 직군의 팀원에게 물어보는게 팀 전체적으로 더 도움이 된다고 생각한다.
이 책의 제목인 함께 자라기
는 함께(협력)과 자라기(학습)을 내포하고 있다.
협력과 학습에 대한 중요성을 여러 가지 상황을 예시로 들면서, 좋은 해결 방안을 알려주고 있는 내용이었다.
이후에 팀 프로젝트를 진행한다면, 이 책을 다시 한번 복기해봐야 겠다는 생각이 들 정도로, 나에게 도움을 많이 준 책이다.
이 글은 내 코드가 그렇게 이상한가요? 책을 읽고 정리한 내용을 바탕으로 작성하였습니다.
소프트웨어 설계에서
결합도
는 모듈 사이의 의존도를 나타내는 지표이고,책무
는 어떤 관심사를 정상적으로 작동하게 제어하는 책임이다.
단일 책임 원칙
을 기반으로 설계해야 한다.
단일 책임 원칙
은 ‘클래스가 담당하는 책임은 하나로 제한해야 한다’는 설계 원칙이다.
책임을 대신 지는 클래스가 만들어지면, 다른 클래스가 제대로 성장할 수 없다. (= 성숙해지지 않는다)
따라서 관심사에 따라 분리해서 독립되어 있는 구조, 즉 느슨한 결합
으로 설계해야한다.
DRY 원칙(= Don’t Repeat Yourself) : ‘빈복을 피해라’
DRY는 각각의 개념 단위내에서 반복을 하지 말라는 의미이다.
같은 로직, 비슷한 로직이라도 개념이 다르면 중복을 허용해야 한다.
무리하게 중복을 제거하려 하면, 강한 결합 상태가 된다.
상속을 하면, 강한 결합 구조를 유발하게 된다. (이 책에서는 상속 자체를 권장하지 않는다)
상속 관계에서 서브 클래스는 슈퍼 클래스에 크게 의존하게 된다. -> 서브 클래스가 슈퍼 클래스의 구조를 하나하나 신경 써야 한다.
강한 결합을 피하려면, 상속보다 컴포지션
을 사용하는 것이 좋다.
컴포지션
이란 사용하고 싶은 클래스를 pri-vate 인스턴스 변수로 갖고 사용하는 것을 의미한다.
상속은 전략 패턴 등으로 조건 분기를 줄일 때 활용할 수 있다.
만약 상속을 사용한다면, 반드시 단일 책임 원칙
을 염두에 두고 구현해야 하며, 값 객체와 컴포지션 등 다른 설계를 사용할 수는 없는지 검토한다.
관련된 것끼리 클래스로 분리한다.
특별한 이유 없이 public
을 사용하지 않는다. -> public
을 붙이면, 강한 결합 구조가 되어버린다. -> 유지보수가 어려워진다.
package private
(default)로 만들고, 패키지 외부에 공개할 필요가 있는 클래스에 한해서만 public
으로 선언한다.한 클래스 내에서 private 메서드가 많으면, 책임이 너무 많은 건 아닌지 확인해본다. -> 책임이 다른 메서드는 다른 클래스로 분리한다.
스마트 UI
는 화면 표시를 담당하는 클래스 중에서 화면 표시와 직접적인 관련이 없는 책무가 구현되어 있는 클래스를 말한다.
트랜잭션 스크립트 패턴
은 메서드 내부에 일련의 처리가 하나하나 길게 작성되어 있는 구조이다.
갓 클래스
는 하나의 클래스 내부에 수천에서 수만 줄의 로직을 담고 있으며, 수많은 책임을 담당하는 로직이 난잡하게 섞여있는 클래스이다.
강한 결합 클래스 대처 방법 -> 객체 지향 설계와 단일 책임 원칙에 따라 제대로 설계한다.
200줄
, 일반적으로 100줄
정도이다.데드코드
는 절대로 실행되지 않는 조건 내부에 있는 코드를 말한다. -> 발견하는 즉시 제거한다.
YAGNI
: ‘You Aren’t Gonna Need it.’ -> 지금 필요 없는 기능을 만들지 말라 -> 지금 당장 필요한 것들만 만들라는 방침이다.
매직 넘버
는 로직 내부에 직접 작성되어 있어서, 의미를 알기 힘든 숫자를 말한다. -> 구현자 본인만 의도를 이해할 수 있다.
영향 범위가 가능한 한 되도록 좁게 설계해야 한다. -> 호출할 수 있는 위치가 적고 국소적일수록 로직을 이해하고 구현하기 쉽다.
null을 리턴/전달하지 않는다. -> null 대신 static final 인스턴스 변수 EMPTY로 만든다.
예외를 확인했다면, 바로 기록하는 것이 좋다.
비즈니스 클래스는 비즈니스 개념을 기준으로 폴더를 구분하는 것이 좋다. -> 도메인 별로 구분되어 응집도가 높아진다.
이 책에서 설명하는 방법들은 사양 변경이 있을 때, 이를 조금이라도 쉽게 하기 위한 설계를 설명한다.
결합이 느슨하고 응집도가 높은 구조로 만들기 위해서는 관심사 분리로, 관심사에 따라서 각각 클래스로 분할해야 한다.
관심사 분리
는 ‘관심사(유스케이스, 목적, 역할)에 따라서 분리한다’라는 소프트웨어 공학의 개념이다.이름 설계하기
최대한 구체적이고, 의미 범위가 좁고, 특화된 이름을 선택하기 -> 어떤 비즈니스를 하는지 모두 파악해야 한다.
존재가 아니라 목적을 기반으로 생각하기 예시) 상품
이라면 -> 입고 상품, 예약 상품, 주문 상품, 발송 상품
어떤 관심사가 있는지 분석하기
소리 내어 이야기해 보기 -> 비즈니스 측면을 잘 이해하고 있는 사람과 이야기 해본다. -> 잘못 인식하는 부분이 있으면 바로 피드백을 받을 수 있다.
이용 약관 읽어보기 -> 비즈니스 규칙과 클래스를 일치하게 만든다.
다른 이름으로 대체할 수 없는지 검토하기 -> ‘고객’이 아니라 ‘투숙객’, ‘결제자’처럼 대체할 수 없도록 이름을 변경한다.
결합이 느슨하고 응집도가 높은 구조인지 검토하기
이름
을 기반으로 메서드와 클래스를 설계해야 한다. -> 프로그램 구조를 크게 좌우한다.
수식어를 붙이면서까지 차이를 나타내고 싶은 대상은 각각 클래스로 설계하는 것이 좋다. -> 의미가 다른 개념을 서로 다른 클래스로 설계해서 구조화하면, 개념 사이의 관계를 이해하기 쉽다.
OriginalMaxHitPoint : ‘캐릭터의 원래 최대 히트 포인트’를 나타내는 클래스
CorrectedMaxHitPoint : ‘장비 착용으로 높아진 최대 히트 포인트’를 나타내는 클래스
DTO(Data Transfer Object) : 예외적으로 데이터 클래스를 사용하는 경우
메서드의 이름은 동사 하나로 구성되게 한다.
실제 내용과 내용이 다른 주석은 제거한다.
주석 규칙
로직을 변경할 때는 반드시 주석도 함께 변경해야 한다. -> 주석을 제대로 변경하지 않으면, 실제 로직과 달라져 주석을 읽는 사람에게 혼란을 준다.
로직의 내용을 단순하게 설명하기만 하는 주석은 달지 않는다. -> 시간이 지남에 따라 내용이 낡은 주석이 될 가능성이 높다.
로직의 의도와 사양을 변경할 때 주의할 점을 주석으로 달아야 한다. -> 유지 보수와 사양 변경에 도움이 된다.
문서 주석
이란 특정 형식에 맞춰 주석을 작성하면, API 문서를 생성해 주거나 코드 에디터에서 주석의 내용을 팝업으로 표시해 주는 기능입니다.
Javadoc
을 참고하자.getter/setter는 값을 마음대로 변경할 수 있으므로 잘못된 값이 섞일 수도 있고, 코드가 중복되는 등 응집도를 낮출 수 있다.
매개변수에 final 수식자를 붙여서, 불변으로 만든다.
출력 매개변수는 사용하지 않는 것이 좋다. -> 가독성 저하
매개변수는 최대한 적게 설계한다. -> 메서드가 처리할 게 많아지면, 그만큼 로직이 복잡해진다.
오류는 특정 값으로 리턴하지 말고, 곧바로 예외를 발생시키는 것이 좋다. -> throw new IllegalArgumentException()