<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>java_dong.log</title>
        <link>https://velog.io/</link>
        <description>자바를 사랑합니다</description>
        <lastBuildDate>Sun, 16 Mar 2025 03:02:19 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>java_dong.log</title>
            <url>https://velog.velcdn.com/images/java_dong/profile/cda714f8-b8fc-41a5-8a44-a4b42ee13f86/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. java_dong.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/java_dong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[QueryDSL 중급]]></title>
            <link>https://velog.io/@java_dong/QueryDSL-%EC%A4%91%EA%B8%89</link>
            <guid>https://velog.io/@java_dong/QueryDSL-%EC%A4%91%EA%B8%89</guid>
            <pubDate>Sun, 16 Mar 2025 03:02:19 GMT</pubDate>
            <description><![CDATA[<h1 id="프로젝션과-결과-반환">프로젝션과 결과 반환</h1>
<p><strong>프로젝션 대상이 하나</strong></p>
<pre><code>List&lt;String&gt; result = queryFactory
                    .select(member.username)
                    .from(member)
                    .fetch();</code></pre><ul>
<li>프로젝션 대상이 하나면 타입을 명확히 지정할 수 있음</li>
<li>프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회</li>
</ul>
<p><strong>튜플 조회</strong></p>
<ul>
<li>프로젝션 대상이 둘 이상일 때 사용</li>
</ul>
<pre><code>List&lt;Tuple&gt; result= queryFactory
.select(member.username,member.age)
.from(member)
.fetch();</code></pre><p><br><br></p>
<h1 id="프로젝션-결과-dto-조회">프로젝션 결과 DTO 조회</h1>
<p><strong>MemberDto</strong></p>
<pre><code>@Data
public class MemberDto {
    private String username;
    private int age;

    public MemberDto() {
    }

    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}</code></pre><p><strong>DTO 조회 코드</strong></p>
<pre><code>List&lt;MemberDto&gt; result = em.createQuery(
&quot;select new study.querydsl.dto.MemberDto(m.username,m.age)&quot; +
&quot;from Member m&quot;,MemberDto.class)
.getresultList();</code></pre><ul>
<li>순수 <code>JPA</code>에서는 <code>DTO</code>를 조회할때 new 명령어를 사용해야함</li>
<li>DTO의 패키지명과 생성자 방식만 지원해서 더러움</li>
</ul>
<h2 id="querydsl-빈-생성">Querydsl 빈 생성</h2>
<ul>
<li>프로퍼티 접근</li>
<li>필드 직접 접근</li>
<li>생성자 사용</li>
</ul>
<p><strong>프로퍼티 접근 setter</strong></p>
<pre><code>List&lt;MemberDto&gt; result =queryFactory.
select(Projections.bean(MemberDto.class,member.username,member.age))
.from(member)
.fetch();</code></pre><p><strong>필드 직접 접근</strong></p>
<pre><code>List&lt;MemberDto&gt; result =queryFactory
.select(Projections.fields(MemberDto.class,member.suername,member.age))
.from(member)
.fetch();</code></pre><p><strong>별칭이 다를 때</strong></p>
<pre><code>@Data
public class UserDto {
    private String name;
    private int age;

    public UserDto() {
    }

    public UserDto(String name, int age) {
        this.name = name;
        this.age = age;
    }
}</code></pre><ul>
<li>코드</li>
</ul>
<pre><code>List&lt;UserDto&gt; fetch =queryFactory
.select(Projections.fields(UserDto.class,member.username,as(&quot;name&quot;),
ExpressionUtils.as(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub),&quot;age&quot;))
.from(member)
.fetch();</code></pre><ul>
<li><code>ExpressionUtils.as(source,alias)</code>: 필드나 서브 쿼리에 별칭 적용</li>
<li><code>username.as(&quot;memberName&quot;)</code>: 필드에 별칭 적용</li>
</ul>
<p><strong>생성자 사용</strong></p>
<pre><code>List&lt;MemberDto&gt; result =queryFactory
.select(Projections.constructor(MemberDto.class,member.username,member.age))
.from(member)
.fetch();</code></pre><h2 id="queryprojection">@QueryProjection</h2>
<pre><code>@Data
public class MemberDto {
    private String username;
    private int age;

    public MemberDto() {
    }
    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}</code></pre><ul>
<li>이때 <code>QMemberDto</code> 생성 확인</li>
</ul>
<p><strong>@QueryProjection 활용</strong></p>
<pre><code>List&lt;MemberDto&gt; result =queryFactory
.select(new QMemberDto(member.username,member.age))
.from(member)
.fetch();</code></pre><p><br><br></p>
<h1 id="동적쿼리">동적쿼리</h1>
<pre><code>@Test
    void dynamicQuery_BooleanBuilder(){
        String usernameParam = &quot;member1&quot;;
        Integer ageParam=10;

        List&lt;Member&gt; result = searchMember1(usernameParam,ageParam);
        assertThat(result.size()).isEqualTo(1);
    }

    private List&lt;Member&gt; searchMember1(String usernameParam, Integer ageParam) {
        BooleanBuilder builder = new BooleanBuilder();
        if(usernameParam!=null){
            builder.and(member.username.eq(usernameParam));
        }
        if(ageParam!=null){
            builder.and(member.age.eq(ageParam));
        }

        return queryFactory.selectFrom(member).where(builder).fetch();
    }</code></pre><ul>
<li><code>BooleanBuilder</code>로 동적쿼리 설정</li>
</ul>
<p><strong>where 다중 파라미터 사용</strong></p>
<pre><code> @Test
    void dynamicQuery_whereParam(){
        String usernameParam = &quot;member1&quot;;
        Integer ageParam=10;

        List&lt;Member&gt; result = searchMember2(usernameParam,ageParam);
        assertThat(result.size()).isEqualTo(1);
    }

    private List&lt;Member&gt; searchMember2(String usernameParam, Integer ageParam) {
        return queryFactory.selectFrom(member)
                .where(usernameEq(usernameParam),ageEq(ageParam))
                .fetch();
    }

    private BooleanExpression ageEq(Integer ageParam) {
        return ageParam!=null ? member.age.eq(ageParam) : null;
    }

    private BooleanExpression usernameEq(String usernameParam) {
        return usernameParam==null ? null :  member.username.eq(usernameParam);
    }
</code></pre><ul>
<li><code>where</code> 조건에 <code>null</code>값은 무시</li>
<li>메서드를 다른 쿼리에서도 재활용 가능</li>
<li>쿼리자체의 가독성이 높아짐</li>
</ul>
<p><strong>조합 가능</strong></p>
<pre><code>private BooleanExpression allEq(String usernameParam, Integer ageParam) {
        return usernameEq(usernameParam).and(ageEq(ageParam));
    }</code></pre><p><br><br></p>
<h1 id="수정삭제-벌크-연산">수정/삭제 벌크 연산</h1>
<p><strong>쿼리한번으로 대량 데이터 수정</strong></p>
<pre><code>long count = queryFactory.update(member).set(member.username,&quot;비회원&quot;)
                        .where(member.age.lt(28)).execute();

        em.flush();
        em.clear();</code></pre><p><strong>기존 숫자에 더하기/곱하기</strong></p>
<pre><code>long count =queryFactory.update(member).set(member.age,member.age.add(1)).execute();
long count2 =queryFactory.update(member).set(member.age,member.age.multiply(2)).execute();</code></pre><p><strong>대용량 삭제</strong></p>
<pre><code>long cnt= queryFactory.delete(member).where(member.age.gt(18)).execute();</code></pre><p>출처: <a href="https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84">https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트로 정리하는 리액트(4)]]></title>
            <link>https://velog.io/@java_dong/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-%EB%A6%AC%EC%95%A1%ED%8A%B84</link>
            <guid>https://velog.io/@java_dong/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-%EB%A6%AC%EC%95%A1%ED%8A%B84</guid>
            <pubDate>Sun, 16 Mar 2025 02:16:40 GMT</pubDate>
            <description><![CDATA[<h1 id="수정-기능">수정 기능</h1>
<p>** Edit 페이지**</p>
<pre><code>const Edit=()=&gt;{
    const nav =useNavigate();
    const {onDelete} = useContext(DiaryDispatchContext);
    const {onUpdate}=useContext(DiaryDispatchContext);
    const data = useContext(DiaryStateContext);
    const params =useParams();
    const [curDiaryItem,setCurDiaryItem] = useState();
    useEffect(()=&gt;{
        const currentDiaryItem = data.find((item)=&gt;String(item.id)===String(params.id));
        if(!currentDiaryItem){
            window.alert(&#39;존재하지 않는 일기입니다&#39;);
            nav(&#39;/&#39;,{replace:true});                      
        }
        setCurDiaryItem(currentDiaryItem);
    },[params.id]);
    const onClickDelete = ()=&gt;{
        if(window.confirm(&#39;정말로 삭제하실겁니까?&#39;)){
            onDelete(params.id);
            nav(&#39;/&#39;,{replace:true});
        }
    }

    const onSubmit=(input)=&gt;{
        if(window.confirm(&quot;일기를 정말 수정?&quot;)){
            onUpdate(params.id,input.createdDate.getTime(),input.emotionId,input.content);
        }
        nav(&#39;/&#39;,{replace:true});
    }
    return(
        &lt;div&gt;
            &lt;Header title={&#39;일기 수정하기&#39;} leftChild={&lt;Button text={&#39;&lt; 뒤로 가기&#39;} onClick={()=&gt;{
                nav(-1)}}&gt;&lt;/Button&gt;} rightChild={&lt;Button text={&#39;삭제하기&#39;} onClick={onClickDelete}&gt;&lt;/Button&gt;}&gt;
            &lt;/Header&gt;
            &lt;Editor onSubmit={onSubmit} initData={curDiaryItem}&gt;&lt;/Editor&gt;
        &lt;/div&gt;
    )
}

export default Edit</code></pre><ul>
<li><code>const currentDiaryItem = data.find((item)=&gt;String(item.id)===String(params.id));</code></li>
<li><code>useContext</code>로 전달 받은 데이터 중에서 아이디값과 파라미터로 받은 아이디 값이 같은 데이터 탐색</li>
<li>파라미터가 달라질 때마다 리렌더링 되어야 하니 <code>params.id</code>로 설정</li>
<li><code>confirm</code>으로 사용자 편리성 증대</li>
</ul>
<p><strong>Editor Component</strong></p>
<pre><code>const Editor=({onSubmit,initData})=&gt;{
    const [differ,setDiffer]=useState();
    useEffect(()=&gt;{
        if(initData){
            setDiffer(initData.emotionId);
            setInput({
                ...initData,
                createdDate: new Date(Number(initData.createdDate))
            })
        }
    },[initData])

    const nav=useNavigate();
    const[input,setInput]=useState({
        createdDate:new Date(),
        emotionId:3,
        content:&quot;&quot;
    });
   console.log(initData);


    const onClickSubmit=()=&gt;{
        onSubmit(input);
    }

    const onChangeInput=(e)=&gt;{
        let name=e.target.name;
        let value=e.target.value;
        if(name===&#39;createdDate&#39;){
            value= new Date(value);
        }

        setDiffer(null);

        setInput({...input,[name]:value});

    }

    return(
        &lt;div&gt;
            &lt;section&gt;
                &lt;h4&gt;오늘의 날짜&lt;/h4&gt;
                &lt;input type=&quot;date&quot; name=&quot;createdDate&quot; id=&quot;&quot; value={getStringedDate(input.createdDate)} onChange={onChangeInput}/&gt;
            &lt;/section&gt;
            &lt;section&gt;
                &lt;h4&gt;오늘의 감정&lt;/h4&gt;
                &lt;div&gt;
                    {emotionList.map((item)=&gt; &lt;EmotionItem differ={differ} key={item.emotionId}{...item} isSelected={item.emotionId===input.emotionId} onClick={()=&gt;onChangeInput({
                        target:{
                            name:&#39;emotionId&#39;,
                            value:item.emotionId
                        }
                    })}&gt;&lt;/EmotionItem&gt; )}
                &lt;/div&gt;
            &lt;/section&gt;
            &lt;section&gt;
                &lt;h4&gt;오늘의 일기&lt;/h4&gt;
                &lt;textarea name=&quot;content&quot; id=&quot;&quot; value={input.content} onChange={onChangeInput} placeholder=&quot;오늘은 어땟나요? &quot;&gt;&lt;/textarea&gt;
            &lt;/section&gt;
            &lt;section&gt;
                &lt;Button text={&#39;취소하기&#39;} onClick={()=&gt;{
                    nav(-1);
                }}&gt;&lt;/Button&gt;
                &lt;Button onClick={()=&gt;{
                    onClickSubmit();
                    nav(-1,{replace:true})
                }} text={&#39;작성완료&#39;}&gt;&lt;/Button&gt;
            &lt;/section&gt;
        &lt;/div&gt;
    )
}

export default Editor</code></pre><ul>
<li><p><code>initData</code>를 값으로 전달 받아 수정하기를 누르면 기존에 입력되어있던 데이터가 화면상에 표시되도록 만듦</p>
</li>
<li><p><code>onChangeInput</code>에서 사용자가 <code>EmotionItem</code>을 클릭하면 <code>differ</code>를 초기화 함</p>
</li>
<li><p>이는 후에 나오는 <code>EmotionItem</code> 컴포넌트에서 기존에 입력되었던 <code>emotionId</code>를 초기화하여 클릭시 기존에 있던 배경색은 사라지게 만들고 새로 클릭한 배경색이 되게 만듦</p>
</li>
</ul>
<p><strong>EmotionItem Component</strong></p>
<pre><code>const EmotionItem=({emotionId,emotionName,isSelected,onClick,differ})=&gt;{

    const isDefault = differ !== null &amp;&amp; differ === emotionId;


    console.log(isDefault);
    return(
        &lt;div&gt;
            &lt;div className={`EmotionItem ${isSelected ? `EmotionItem_on_${emotionId}`:&quot;&quot;} ${isDefault ? `EmotionItem_default_${emotionId}`:&quot;&quot;}`} tabIndex=&quot;0&quot; onClick={onClick}&gt;
                &lt;img className=&quot;emotion_img&quot; src={getEmotionImage(emotionId)} alt=&quot;&quot; /&gt;
                &lt;div className=&quot;emotion_name&quot;&gt;
                    {emotionName}
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    )
}

export default EmotionItem</code></pre><ul>
<li><code>const isDefault = differ !== null &amp;&amp; differ === emotionId;</code>: <code>null</code>값이면 false</li>
<li><code>{isDefault ? EmotionItem_default_${emotionId}:&quot;&quot;}</code>}: <code>true</code> 일 때만 해당 css가 적용됨</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[QueryDSL 시작]]></title>
            <link>https://velog.io/@java_dong/QueryDSL-%EC%8B%9C%EC%9E%91</link>
            <guid>https://velog.io/@java_dong/QueryDSL-%EC%8B%9C%EC%9E%91</guid>
            <pubDate>Sat, 15 Mar 2025 10:33:37 GMT</pubDate>
            <description><![CDATA[<h1 id="예제-도메인-모델">예제 도메인 모델</h1>
<p><strong>Member</strong></p>
<pre><code>@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of={&quot;id&quot;,&quot;username&quot;,&quot;age&quot;})
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;team_id&quot;)
    private Team team;

    public Member(String username) {
        this(username, 0);
    }

    public Member(String username, int age) {
        this(username, age, null);
    }
    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}
</code></pre><ul>
<li><code>ToString</code>은 가급적이면 연관관계에 해당하지 않는 걸로 설정</li>
<li><code>NoArgsConstructor</code> =&gt; <code>protected</code>로 외부에서 접근 못하게 막음</li>
<li><code>changeTeam</code>으로 양방향 연관관계 한번에 처리하기</li>
</ul>
<p><strong>Team</strong></p>
<pre><code>@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {&quot;id&quot;, &quot;name&quot;})
public class Team {
    @Id
    @GeneratedValue
    @Column(name = &quot;team_id&quot;)
    private Long id;
    private String name;

    @OneToMany(mappedBy = &quot;team&quot;)
    private List&lt;Member&gt; members=new ArrayList&lt;&gt;();

    public Team(String name) {
        this.name = name;
    }
}
</code></pre><ul>
<li>Member와 Team의 양방향 연관관계 설정</li>
<li>하지만 연관관계의 주인은 <code>Member</code>이므로 <code>Team</code>에서는 조회만 가능함</li>
</ul>
<p><br><br></p>
<h1 id="기본-문법">기본 문법</h1>
<p><strong>JPQL</strong></p>
<pre><code>@Test
    void startJpql(){
        String qlString= &quot;select m from Member m &quot; +
                &quot;where m.username = :username&quot;;
        Member findMember = (Member)em.createQuery(qlString).setParameter(&quot;username&quot;,&quot;member1&quot;).getSingleResult();
        assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);
    }</code></pre><ul>
<li>사전에 저장된 회원 값 찾기</li>
</ul>
<p><strong>QueryDSL</strong></p>
<pre><code>  @Test
    void startQuerydls(){

        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m= new QMember(&quot;m&quot;);
        Member findMember = queryFactory.select(m)
                .from(m)
                .where(m.username.eq(&quot;member1&quot;))
                .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);

    }</code></pre><ul>
<li><code>EntityManager</code>로 <code>JPAQueryFactory</code> 생성</li>
</ul>
<pre><code>@BeforeEach
    public void before() {
        queryFactory = new JPAQueryFactory(em);
//...
}
</code></pre><ul>
<li>쿼리팩토리를 필드로 제공하면 <code>EntityManager</code>가 크랜잭션 마다 별도의 영속성 컨텍스트를 제공하므로 동시성 문제는 걱정하지 않아도 됨</li>
</ul>
<p><strong>기본 Q-Type 활용</strong></p>
<pre><code>QMember qMember= new QMember(&quot;m&quot;);
QMember qMember = QMember.member </code></pre><ul>
<li>기본 인스턴스를 <code>static import</code>와 함께 사용 가능</li>
<li>같은 테이블을 조인해야 하는 경우가 아니라면 기본 인스턴스를 권장한다</li>
</ul>
<h2 id="검색조건-쿼리">검색조건 쿼리</h2>
<pre><code>@Test
    void search(){
        Member findMember= queryFactory.select(member).from(member).where(member.username.eq(&quot;member1&quot;).and(member.age.eq(10))).fetchOne();
        assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);
    }
</code></pre><ul>
<li>검색조건은 <code>and,or</code>를 메서드 체인으로 연결이 가능</li>
<li><code>select,from</code>을 <code>select from</code>으로 합치기도 가능</li>
</ul>
<h2 id="검색조건-예시">검색조건 예시</h2>
<ul>
<li><code>eq(member1)</code>: <code>=</code></li>
<li><code>ne(member1)</code>: <code>!=</code></li>
<li><code>eq(member1).not()</code>: <code>!=</code></li>
<li><code>isNotNull()</code>: <code>is not null</code></li>
<li><code>in(a,b)</code></li>
<li><code>notIn(a,b)</code></li>
<li><code>between(a,b)</code></li>
<li><code>goe(10)</code> : <code>&gt;=</code></li>
<li><code>gt(10)</code>: <code>&gt;</code></li>
<li><code>loe(10)</code>: <code>&lt;=</code></li>
<li><code>lt(10)</code>: <code>&lt;</code></li>
<li><code>like(member1%)</code></li>
<li><code>contains(member)</code> : <code>like %member1%</code></li>
<li><code>startsWith(member)</code>: <code>like member%</code> </li>
</ul>
<pre><code> @Test
    void searchAndParam(){
        Member findMember = queryFactory.selectFrom(member).where(member.username.eq(&quot;member1&quot;),member.age.eq(10)).fetchOne();

        assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);
    }</code></pre><ul>
<li><code>where()</code>에 파라미터로 검색조건을 추가하면 <code>And</code> 조건이 추가됨</li>
<li>이 경우 <code>null</code>값은 무시 =&gt; 메서드 추출을 활용해서 깔끔한 동적 쿼리를 만들 수 있다</li>
</ul>
<h2 id="결과조회">결과조회</h2>
<ul>
<li><code>fetch()</code>: 리스트 조회, 데이터 없으면 빈 리스트 반환</li>
<li><code>fetchOne()</code>: 단건 조회<ul>
<li>결과가 없으면 <code>null</code></li>
<li>결과가 둘 이상이면 예외 반환</li>
</ul>
</li>
<li><code>fetchFirst()</code>: <code>limit(1).fetchOne()</code></li>
<li><code>fetchResults()</code>: 페이징 정보 포함, total count 쿼리 추가 실행</li>
<li><code>fetchCount()</code>: count 쿼리로 변경해서 count 수 조회</li>
</ul>
<h2 id="정렬">정렬</h2>
<pre><code>@Test
    void sort(){
        em.persist(new Member(null,100));
        em.persist(new Member(&quot;member5&quot;,100));
        em.persist(new Member(&quot;member6&quot;,100));

        List&lt;Member&gt; result = queryFactory.selectFrom(member).where(member.age.eq(100)).orderBy(member.age.desc(), member.username.asc().nullsLast()).fetch();
        System.out.println(result.toString());
        Member member5 = result.get(0);
        Member member6 = result.get(1);
        Member member7 = result.get(2);
        assertThat(member5.getUsername()).isEqualTo(&quot;member5&quot;);
        assertThat(member6.getUsername()).isEqualTo(&quot;member6&quot;);
        assertThat(member7.getUsername()).isNull();
    }</code></pre><ul>
<li><code>desc(),asc()</code>: 일반 정렬</li>
<li><code>nullsLast(),nullsFirst()</code>: null 데이터 순서 부여</li>
</ul>
<h2 id="페이징">페이징</h2>
<p><strong>조회 건수 제한</strong></p>
<pre><code> @Test
    void paging() {

        List&lt;Member&gt; result = queryFactory.selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)
                .limit(2)
                .fetch();
    }</code></pre><ul>
<li><code>offset(1)</code>: 0부터 조회</li>
<li><code>limit(2)</code>: 최대 2건 조회</li>
</ul>
<p>*<em>전체 조회 수 *</em></p>
<pre><code> @Test
    void paging2(){
        QueryResults&lt;Member&gt; result = queryFactory.selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)
                .limit(2)
                .fetchResults();
        assertThat(result.getTotal()).isEqualTo(4);
        assertThat(result.getLimit()).isEqualTo(2);
        assertThat(result.getOffset()).isEqualTo(1);

    }</code></pre><h2 id="집합">집합</h2>
<p><strong>집합 함수</strong></p>
<ul>
<li><code>COUNT(m)</code> : 회원 수</li>
<li><code>SUM(m.age)</code>: 나이 합</li>
<li><code>AVG(m.age)</code>: 평균 나이</li>
<li><code>MAX(m.age)</code>: 최대 나이</li>
<li><code>MIN(m.age)</code>: 최소 나이</li>
</ul>
<p><strong>사용 예시</strong></p>
<pre><code>@Test
    void aggregation(){
        List&lt;Tuple&gt; fetch = queryFactory.select(member.count(), member.age.sum(), member.age.avg(), member.age.max(), member.age.min()).from(member).fetch();
        Tuple tuple = fetch.get(0);
        assertThat(tuple.get(member.count())).isEqualTo(4) ;
        assertThat(tuple.get(member.age.sum())).isEqualTo(100);
    }</code></pre><h2 id="group-by">group by</h2>
<pre><code> @Test
    void group() throws Exception{
        List&lt;Tuple&gt; result = queryFactory.select(team.name,member.age.avg())
                .from(member).join(member.team,team).groupBy(team.name).fetch();

        Tuple teamA = result.get(0);
        Tuple teamB = result.get(1);
        assertThat(teamA.get(team.name)).isEqualTo(&quot;teamA&quot;);
        assertThat(teamA.get(member.age.avg())).isEqualTo(15);
        assertThat(teamB.get(team.name)).isEqualTo(&quot;teamB&quot;);
        assertThat(teamB.get(member.age.avg())).isEqualTo(35);
    }</code></pre><ul>
<li>그룹화의 결과를 제한하려면 <code>Having</code> 사용</li>
</ul>
<h2 id="조인---기본-조인">조인 - 기본 조인</h2>
<pre><code>join(조인대상, 별칭으로 사용할 Q 타입)</code></pre><p><strong>기본조인</strong></p>
<pre><code>@Test
    void join(){
        List&lt;Member&gt;result= queryFactory.select(member).from(member)
                .groupBy(member.team,team).where(team.name.eq(&quot;teamA&quot;)).fetch();

        assertThat(result).extracting(&quot;username&quot;).containsExactly(&quot;member1&quot;,&quot;member2&quot;);
    }</code></pre><ul>
<li><code>join(),innerJoin()</code>: 내부조인</li>
<li><code>leftJoin</code>: left 외부 조인</li>
<li><code>rightJoin()</code>; right 외부 조인</li>
</ul>
<p><strong>세타조인</strong></p>
<pre><code> @Test
    void setterJoin(){
        em.persist(new Member(&quot;teamA&quot;));
        em.persist(new Member(&quot;teamB&quot;));
        List&lt;Member&gt; result = queryFactory.select(member).from(member, team).where(member.username.eq(team.name)).fetch();

        assertThat(result).extracting(&quot;username&quot;).containsExactly(&quot;teamA&quot;,&quot;teamB&quot;);
    }</code></pre><ul>
<li>연관관계가 없는 필드로 조인</li>
<li>from 절에 여러 엔터티를 선택해서 세타 조인</li>
</ul>
<p><strong>조인 - ON 절</strong></p>
<pre><code>@Test
    void join_on_filterring(){
        List&lt;Tuple&gt; result = queryFactory.select(member,team).from(member)
                .leftJoin(member.team,team).on(team.name.eq(&quot;teamA&quot;)).fetch();

        for(Tuple tuple : result){
            System.out.println(&quot;tuple : &quot;+tuple);
        }
    }</code></pre><ul>
<li>회원은 모두 조회하면서 팀 이름이 <code>teamA</code>인 팀만 조인</li>
</ul>
<p><strong>연관관계 없는 조인</strong></p>
<pre><code> @Test
    void join_on_no_realation(){
        em.persist(new Member(&quot;teamA&quot;));
        em.persist(new Member(&quot;teamB&quot;));
        List&lt;Tuple&gt; result = queryFactory.select(member,team).from(member)
                .leftJoin(member.team,team).on(member.username.eq(&quot;teamA&quot;)).fetch();

        for(Tuple tuple : result){
            System.out.println(&quot;tuple : &quot;+tuple);
        }
    }</code></pre><ul>
<li>주의: 문법을 잘 봐야 한다 <code>leftJoin()</code> 부분에 일반 조인과 다르게 엔터티 하나만 들어감</li>
</ul>
<p><strong>조인 - 페치조인</strong></p>
<pre><code> @Test
    void fetchJoin(){
        em.flush();
        em.clear();
        Member findMember = queryFactory.selectFrom(member).join(member.team,team).fetchJoin().where(member.username.eq(&quot;member1&quot;))
                .fetchOne();
        boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());

    }</code></pre><ul>
<li>일반조인을 사용하면 사용하지 않는 엔터티인 <code>Team</code>까지 조회하지 않음</li>
<li>페치 조인을 사용하면 <code>Team</code>까지 한꺼번에 가져옴</li>
</ul>
<h2 id="서브-쿼리">서브 쿼리</h2>
<p><strong>예시 1</strong></p>
<pre><code> @Test
    void subQuery(){
        QMember qmember= new QMember(&quot;memberSub&quot;);
        List&lt;Member&gt; result = queryFactory.select(member).from(member)
                        .where(member.age.eq(JPAExpressions.select(qmember.age.max())
                                .from(qmember))).fetch();
        assertThat(result).extracting(&quot;age&quot;).containsExactly(40);
    }</code></pre><ul>
<li>나이가 가장많은 회원 조회</li>
<li><code>JPAExpressions</code> 사용</li>
</ul>
<p>*<em>예시 2 *</em></p>
<pre><code>@Test
    void subQueryGoe(){
        QMember memberSub =new QMember(&quot;memberSub&quot;);
        List&lt;Member&gt; result = queryFactory.selectFrom(member).where(member.age.goe(
                JPAExpressions.select(memberSub.age.avg())
                        .from(memberSub)
        )).fetch();
        assertThat(result).extracting(&quot;age&quot;).containsExactly(30,40);
    }</code></pre><ul>
<li>나이가 전체 회원의 평균 이상인 사람</li>
</ul>
<p><strong>In 서브 쿼리</strong></p>
<pre><code> @Test
    void subQueryIn(){
        QMember memberSub =new QMember(&quot;memberSub&quot;);
        List&lt;Member&gt; result = queryFactory.selectFrom(member)
                .where(member.age.in(
                        JPAExpressions.select(memberSub.age).from(memberSub)
                                .where(memberSub.age.gt(10))
                )).fetch();
        assertThat(result).extracting(&quot;age&quot;).containsExactly(20,30,40);
    }</code></pre><h2 id="case문">CASE문</h2>
<p><strong>단순한 조건</strong></p>
<pre><code> @Test
    void basicCase(){
        List&lt;String&gt; result =queryFactory.select(member.age
                        .when(10).then(&quot;10살&quot;)
                        .when(20).then(&quot;20살&quot;)
                        .otherwise(&quot;기타&quot;))
                .from(member)
                .fetch();

        for(String str : result){
            System.out.println(&quot;s =&quot; +str);
        }

    }</code></pre><p><strong>복잡한 조건</strong></p>
<pre><code>@Test
    void complexCase(){
        List&lt;String&gt; result = queryFactory.select(new CaseBuilder()
                        .when(member.age.between(0, 20)).then(&quot;0~20살&quot;)
                        .when(member.age.between(21, 30)).then(&quot;21~30살&quot;)
                        .otherwise(&quot;기타&quot;))
                .from(member)
                .fetch();
        for(String str : result){
            System.out.println(&quot;s =&quot; +str);
        }
    }</code></pre><p><strong>orderBy에서 Case문 함께 사용하기</strong></p>
<ol>
<li>0~30살이 아닌 사람을 가장 먼저 출력</li>
<li>0~20살 출력</li>
<li>21~30살 출력</li>
</ol>
<pre><code>NumberExpressions&lt;Integer&gt; rankPath= new CaseBuilder();
                    .when(member.age.between(0,20)).then(2)
                    .when(member.age.between(21,30)).then(1)
                    .otherwise(3);

List&lt;Tuple&gt; result = queryFactory
                   .select(member.username, member.age, rankPath)
                   .from(member)
                   .orderBy(rankPath.desc())
                   .fetch();

 for (Tuple tuple : result) {
     String username = tuple.get(member.username);
     Integer age = tuple.get(member.age);
     Integer rank = tuple.get(rankPath);
     System.out.println(&quot;username = &quot; + username + &quot; age = &quot; + age + &quot; rank = &quot; +rank);</code></pre><h2 id="상수-더하기--concatconstant">상수 더하기 -concat/constant</h2>
<pre><code> @Test
    void constant(){
        List&lt;Tuple&gt; result = queryFactory.select(member.username, Expressions.constant(&quot;A&quot;))
                .from(member).fetch();
        for(Tuple tuple : result){
            System.out.println(&quot;tuple : &quot;+tuple);
        }
    }

    @Test
    void concat(){
        List&lt;String&gt; result = queryFactory.select(member.username.concat(&quot;_&quot;).concat(member.age.stringValue())).from(member).where(member.username.eq(&quot;member1&quot;)).fetch();

        for(String s: result){
            System.out.println(&quot;s =&quot; +s);
        }
    }
</code></pre><p><br><br></p>
<p>출처: <a href="https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84">https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체지향 쿼리 언어(JPQL)]]></title>
            <link>https://velog.io/@java_dong/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-%EC%96%B8%EC%96%B4JPQL</link>
            <guid>https://velog.io/@java_dong/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-%EC%96%B8%EC%96%B4JPQL</guid>
            <pubDate>Tue, 11 Mar 2025 03:44:26 GMT</pubDate>
            <description><![CDATA[<h1 id="jpql이란">JPQL이란?</h1>
<p><strong>JPA 쿼리방법</strong></p>
<ul>
<li>JPQL</li>
<li>QueryDSL</li>
</ul>
<p><strong>JPQL</strong></p>
<ul>
<li>JPA를 사용하면 엔터티 객체를 중심으로 개발</li>
<li>검색을 할 때 테이블이 아닌 엔터티 객체를 대상으로 검색</li>
<li>모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능</li>
<li>필요한 데이터만 불러오려면 결국 검색 조건이 포함된 SQL이 필요</li>
<li>JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공</li>
<li>예시<pre><code>String jpql = &quot;select m from Member m where m.age &gt; 18&quot;;
List&lt;Member&gt; result= em.createQuery(jpql,Member.class).getResultList();</code></pre></li>
<li>실행된 쿼리<pre><code>select
m.id as id,
m.age as age,
m.USERNAME as USERNAME,
m.TEAM_ID as TEAM_ID
from
Member m
where
m.age&gt;18</code></pre></li>
<li><em>Criteria*</em></li>
<li>문자가 아닌 자바 코드로 JPQL을 작성할 수 있음</li>
<li>JPQL 빌더 역할</li>
<li>단점은 너무 복잡하고 실용성이 없음</li>
</ul>
<p><strong>QureyDSL</strong></p>
<ul>
<li>문자가 아닌 자바코드로 JPQL을 작성할 수 있음</li>
<li>JPQL 빌더 역할</li>
<li>컴파일 시점에서 문법 오류를 찾을 수 있음</li>
<li>동적쿼리 작성시 편리함</li>
<li>단순하고 쉬움</li>
<li>예시<pre><code>JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m= QMember.member;
List&lt;Member&gt; list= query.selectFrom(m).where(m.age.gt(18))
                  .orderBy(m.name.desc()).fetch();</code></pre><br><br></li>
</ul>
<h1 id="jpql-문법">JPQL 문법</h1>
<pre><code>select_문 :: =
  select_절
  from_절
  [where_절]
  [groupby_절]
  [having_절]
  [orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]</code></pre><ul>
<li><code>select m from Member as m where m.age&gt;18</code></li>
<li>엔터티와 속성은 대소문자를 구분함</li>
<li>JPQL 키워드는 대소문자 구분 x</li>
<li>별칭은 필수 as는 생략 가능</li>
</ul>
<p><strong>집합과 정렬</strong></p>
<pre><code>select
  COUNT(m), //회원수
  SUM(m.age), //나이 합
  AVG(m.age), //평균 나이
  MAX(m.age), //최대 나이
  MIN(m.age) //최소 나이
 from Member m</code></pre><ul>
<li><code>group by</code>,<code>Having</code></li>
<li><code>order by</code></li>
</ul>
<p><strong>TypedQuery/Query</strong></p>
<ul>
<li><code>TypedQuery</code>: 반환 타입이 명확할 때 사용<pre><code>TypedQuery&lt;Member&gt; query =
em.createQuery(&quot;SELECT m FROM Member m&quot;, Member.class);</code></pre></li>
<li><code>Query</code>: 반환 타입이 명확하지 않을 떄 사용<pre><code>Query query =
em.createQuery(&quot;SELECT m.username, m.age from Member m&quot;);</code></pre></li>
</ul>
<p>*<em>결과 조회 API *</em></p>
<ul>
<li><code>query.getResultList()</code>: 결과가 하나 이상일때 리스트 반환<ul>
<li>결과가 없으면 빈 리스트 반환</li>
</ul>
</li>
<li><code>query.getSingleResult()</code>: 결과가 정확히  하나, 단일 객체 반환</li>
</ul>
<p><strong>파라미터 바인딩 - 이름/위치 기준</strong></p>
<pre><code>SELECT m FROM Member m where m.username=:username
query.setParameter(&quot;username&quot;, usernameParam);

SELECT m FROM Member m where m.username=?1
query.setParameter(1, usernameParam);</code></pre><p><strong>프로젝션</strong></p>
<ul>
<li><code>select</code>절에 조회할 대상을 지정</li>
<li>프로젝션 대상: 엔터티, 임베디드 타입, 스칼라 타입</li>
<li><code>select m.username,m.age From Member m</code></li>
<li>단순값을 바로 DTO로 조회<ul>
<li><code>select new jpaBook.jpql.UserDto(m.username,m.age) from Member m</code></li>
<li>패키지 명을 포함한 전체 클래스 명 입력</li>
<li>순서와 타입이 일치하는 생성자 필요</li>
</ul>
</li>
</ul>
<p><strong>페이징 API</strong></p>
<ul>
<li><code>setFirstResult(int str):</code> 조회시작 위치 (0부터 시작)</li>
<li><code>setMaxResults(int max):</code>조회할 데이터 수</li>
<li>예시<pre><code>String jpql = &quot;select m from Member m order by m.name desc&quot;;
List&lt;Member&gt; resultList = em.createQuery(jpql, Member.class)
.setFirstResult(10)
.setMaxResults(20)
.getResultList();</code></pre></li>
</ul>
<p><strong>조인</strong></p>
<ul>
<li><p>내부조인:  <code>select m from Member m Join m.team t</code></p>
</li>
<li><p>외부조인: <code>select m from Member m outer Join m.team t</code> </p>
</li>
<li><p>세타조인: <code>select count(m) from Member m ,Team t where m.username=t.username</code></p>
</li>
<li><p><code>on절을 활용한 조인</code></p>
<ul>
<li><p>조인대상 필터링</p>
<ul>
<li>예시(회원과 팀을 조인하면서 팀이름이 <code>A</code>인 팀만 조회)</li>
</ul>
<p>SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = &#39;A&#39;</p>
</li>
<li><p>연관관계 없는 엔터티 외부 조인</p>
<ul>
<li>예시(회원 이름과 팀의 이름이 같은 대상 외부 조인)</li>
</ul>
<p>SELECT m, t FROM
Member m LEFT JOIN Team t on m.username = t.name</p>
</li>
</ul>
</li>
</ul>
<p><strong>서브쿼리</strong></p>
<ul>
<li>나이가 평균보다 많은 회원<pre><code>select m from Member m
where m.age &gt; (select avg(m2.age) from Member m2)
- 한건이라도 주문한 고객</code></pre>select m from Member m
where (select count(o) from Order o where m=o.member) &gt; 0</li>
</ul>
<ul>
<li><p>서브쿼리 지원 함수</p>
</li>
<li><p>[NOT] EXISTS(subquery): 서브쿼리에 결과가 존재하면 참</p>
<ul>
<li>ALL/ANY/SOME</li>
<li>ALL 모두 만족하면 참</li>
<li>ANY,SOME : 같은 의미, 조건을 하나라도 만족하면 참</li>
</ul>
</li>
<li><p>[NOT] IN(subquery): 서브쿼리의 결과중 하나라도 같은 것이 있으면 참</p>
</li>
<li><p>예제</p>
<ul>
<li><p>팀 A 소속인 회원</p>
<p>  select m from Member m
  where exists (select t from m.team t where t.name = ‘팀A&#39;)</p>
</li>
<li><p>전체 상품 각각의 재고보다 주문량이 많은 주문들</p>
<pre><code> select o from Order o</code></pre><p>where o.orderAmount &gt; ALL (select p.stockAmount from Product p)</p>
</li>
<li><p>어떤 팀이든 팀에 소속된 회원</p>
<pre><code>        select m from Member m</code></pre><p>where m.team = ANY (select t from Team t)</p>
</li>
</ul>
</li>
</ul>
<ul>
<li>서브 쿼리의 한계<ul>
<li>JPA는 <code>WHERE HAVING</code> 절에서만 서브 쿼리 사용 가능</li>
<li><code>FROM</code> 절의 서브쿼리는 현재 <code>JPQL</code>에서 불가능</li>
</ul>
</li>
</ul>
<p><strong>JPQL 기본함수</strong></p>
<ul>
<li>CONCAT</li>
<li>SUBSTRING</li>
<li>TRIM</li>
<li>LOWER,UPPER</li>
<li>LENGTH</li>
<li>LOCATE</li>
<li>ABS, SQRT, MOD</li>
<li>SIZE, INDEX</li>
</ul>
<p><br><br></p>
<h1 id="경로-표현식">경로 표현식</h1>
<ul>
<li><code>.</code>을 찍어 객체 그래프를 탐색하는 것</li>
<li>용어정리<ul>
<li>상태필드: 단순히 값을 저장하기 위한 필드</li>
<li>연관필드: 연관관계를 위한 필드</li>
</ul>
</li>
</ul>
<p><strong>경로표현식 특징</strong></p>
<ul>
<li><p>상태필드 : 경로탐색의 끝, 탐색 x</p>
</li>
<li><p>단일값 연관 경로: 묵시적 내부 조인 발생, 탐색 0</p>
</li>
<li><p>컬렉션 값 연관 경로: 묵시적 내부 조인 발생 , 탐색 X</p>
<ul>
<li><p>단일 값 연관 경로 탐색 예시</p>
<pre><code>JPQL:select o.member from Order o
SQL: select m.* 
from Orders o
inner join Member m on o.member_id = m.id</code></pre></li>
<li><p>명시적/묵시적 조인</p>
<pre><code>select m from Member m join m.team t (명시적: join 키워드 직접 사용)
select m.team from Member m (묵시적: 경로 표현식에 의해 묵시적으로 SQL 조인발생)</code></pre></li>
<li><p>경로 표현식 - 예제</p>
<pre><code>  select o.member.team from Order o (성공)
select t.members from Team (성공)
select t.members.username from Team t (실패)
select m.username from Team t join t.members m (성공)</code></pre></li>
</ul>
</li>
</ul>
<p><br><br></p>
<h1 id="jpql-패치조인">JPQL: 패치조인</h1>
<ul>
<li><p>SQL 조인 종류 x</p>
</li>
<li><p>JPQL에서 성능 최적화를 위해 제공하는 기능</p>
</li>
<li><p>연관된 엔터티나 컬렉션을 SQL 한번에 함께 조회하는 기능</p>
</li>
<li><p><code>join fetch</code> 명령어 사용</p>
<ul>
<li><p>패치조인 예시</p>
<p>select m from Member m join fetch m.team (JPQL)
select M.<em>,T.</em> from Member m inner join Team t on m.team_id=t.id (SQL)</p>
</li>
<li><p>회원을 조회하면서 연관된 팀도 함께 조회</p>
</li>
<li><p>패치조인과 일반 조인 실행시 차이점은 연관된 엔터티를 함께 조회하지 않는다는 것이다</p>
<p>select t from Team t join t.members m where t.name=&#39;팀A&#39;(JPQL)
select T.* from Team t inner join Member m on t.id = m.team_id (SQL)</p>
</li>
</ul>
</li>
</ul>
<p><strong>패치조인의 특징/한계</strong></p>
<ul>
<li>패치 조인대상에는 별칭을 줄 수 없다</li>
<li>둘 이상의 컬렉션은 패치 조인 x</li>
<li>컬렉션을 페치조인하면 페이징 API를 사용 할 수 없다</li>
<li>엔터티에 직접 적용하는 글로벌 로딩 전략보다 우선시 됨(<code>FetchType.LAZY</code>)</li>
</ul>
<p><br><br></p>
<h1 id="다형성-쿼리">다형성 쿼리</h1>
<p><img src="https://velog.velcdn.com/images/java_dong/post/9d6c41ad-c6e5-4262-9698-518ed54eaa54/image.png" alt=""></p>
<ul>
<li>조회대상을 특정 자식으로 한정하는 <code>TYPE</code><pre><code>[JPQL]
select i from Item i
where type(i) IN (Book, Movie) 
</code></pre></li>
</ul>
<p>[SQL]
select i from i
where i.DTYPE in (‘B’, ‘M’)</p>
<pre><code>
&lt;br&gt;&lt;br&gt;

# 엔터티 직접 사용

- JPQL에서 엔터티를 직접 사용하면 SQL에서 해당 엔터티의 기본값을 사용
</code></pre><p>[JPQL]
select  count(m.id) from Member m 
select count(m) from Member m</p>
<p>[SQL]
select count(m.id) as cnt from Member m</p>
<pre><code>- 엔터티를 파라미터로 전달
</code></pre><p>String jpql = “select m from Member m where m = :member”;
List resultList = em.createQuery(jpql)
.setParameter(&quot;mem</p>
<pre><code>
- 식별자를 직접 전달
</code></pre><p>String jpql = “select m from Member m where m.id = :memberId”;
List resultList = em.createQuery(jpql)
.setParameter(&quot;memberId&quot;, memberId).getResultList();</p>
<pre><code>
- 실행된 SQL</code></pre><p>select m.* from Member m where m.id=?</p>
<pre><code>
&lt;br&gt;&lt;br&gt;

# Named 쿼리

- 미리 정의해서 이름을 부여해두고 사용하는 JPQL
- 정적 쿼리
- 에노테이션 또는 xml에 정의
- 에플리케이션 로딩 시점에 초기화 후 재사용
- 애플리케이션 로딩 시점에 쿼리를 검증
</code></pre><p>@Entity
@NamedQuery(
name = &quot;Member.findByUsername&quot;,
query=&quot;select m from Member m where m.username = :username&quot;)
public class Member {
...
}</p>
<p>List<Member> resultList =
em.createNamedQuery(&quot;Member.findByUsername&quot;, Member.class)
.setParameter(&quot;username&quot;,
&quot;회원1&quot;).getResultList();</p>
<pre><code>
**xml 정의(src/META-INF/...xml)**
</code></pre><?xml version="1.0" encoding="UTF-8"?>
<pre><code>&lt;entity-mappings xmlns=&quot;http://xmlns.jcp.org/xml/ns/persistence/orm&quot; version=&quot;2.1&quot;&gt;
    &lt;named-query name=&quot;Member.findByUsername&quot;&gt;
        &lt;query&gt;&lt;![CDATA[
      select m
      from Member m
      where m.username = :username
      ]]&gt;
          &lt;/query&gt;
    &lt;/named-query&gt;
&lt;named-query name=&quot;Member.count&quot;&gt;
    &lt;query&gt;select count(m) from Member m&lt;/query&gt;
&lt;/named-query&gt;</code></pre></entity-mappings>
```
- xml이 항상 우선권을 가짐
- 운영환경에 따라 다른 xml을 배포 가능


<p><br><br></p>
<h1 id="벌크-연산">벌크 연산</h1>
<ul>
<li>JPA 변경 감지로 순식간에 많은 양의 쿼리가 나갈수 있다<ul>
<li>재고가 10개 미만인 상품을 리스트로 조회<ul>
<li>상품 엔터티 가격 10% 증가</li>
</ul>
</li>
<li>트랜잭션 커밋 시점에 변경감지 동작</li>
</ul>
</li>
<li>변경된 데이터가 100건이면 100번의 UPDATE 쿼리 실행</li>
</ul>
<pre><code>String qlString = &quot;update Product p &quot; +
&quot;set p.price = p.price * 1.1 &quot; +
&quot;where p.stockAmount &lt; :stockAmount&quot;;
int resultCount = em.createQuery(qlString)
.setParameter(&quot;stoc</code></pre><ul>
<li><code>executeUpdate()</code>: 쿼리 한번으로 여러 테이블 로우 변경</li>
<li>벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리<ul>
<li>벌크 연산을 먼저 수행</li>
<li>벌크 연산 수행 후 영속성 컨텍스트 초기화</li>
</ul>
</li>
</ul>
<p>출처:<a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트로 정리하는 리액트(3)]]></title>
            <link>https://velog.io/@java_dong/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-%EB%A6%AC%EC%95%A1%ED%8A%B83</link>
            <guid>https://velog.io/@java_dong/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-%EB%A6%AC%EC%95%A1%ED%8A%B83</guid>
            <pubDate>Mon, 10 Mar 2025 07:11:23 GMT</pubDate>
            <description><![CDATA[<h1 id="조회삭제-기능-구현">조회/삭제 기능 구현</h1>
<h2 id="diary-page">Diary Page</h2>
<pre><code>const Diary =()=&gt;{
    const nav=useNavigate();
    const param = useParams();
    const {onDelete}= useContext(DiaryDispatchContext);

    const data = useContext(DiaryStateContext);
    console.log(data);
    const dataR = data.filter((item)=&gt; String(item.id)===String(param.id));
    const emotionId=dataR[0].emotionId;
    const onClick=()=&gt;{
        if(confirm(&#39;삭제하시기를 원합니까?&#39;)){
            onDelete(Number(param.id));
            alert(&#39;삭제가 완료되었습니다&#39;);
            nav(&#39;/&#39;,{replace:true});
        }
    }
    // console.log(dataR);
    return (
        &lt;div&gt;
            &lt;Header title={&#39;일기장 보기&#39;} leftChild={&lt;Button text={&#39;&lt; 뒤로가기&#39;} onClick={()=&gt; nav(-1)}&gt;&lt;/Button&gt;} rightChild={&lt;Button text={&#39;편집하기&#39;} onClick={()=&gt;nav(`/edit/${param.id}`)}&gt;&lt;/Button&gt;} &gt;&lt;/Header&gt;
            &lt;div&gt;
                &lt;View emotionId={emotionId} dataR={dataR}&gt;&lt;/View&gt;
            &lt;/div&gt;
            &lt;div&gt;
                &lt;Button text={&#39;삭제하기&#39;} onClick={onClick}&gt;&lt;/Button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    )
}

export default Diary;</code></pre><ul>
<li><code>header</code> 부분은 새 일기장 만들기와 동일하게 구축함과 동시에 왼쪽 버튼을 클릭하면 <code>home</code>으로 오른쪽 버튼을 클릭하면 편집하는 페이지로 이동하게 만듦</li>
<li>사용자가 작성한 일기장의 내용은 <code>View</code> 컴포넌트에서 처리하게 만듦</li>
<li>이때 전달한 데이터는 감정아이디와 파라미터로 받은 <code>id</code>로 찾은 데이터를 전달</li>
<li>아래 삭제하기 버튼을 누르면 <code>useContext</code>로 받은 <code>onDelete</code> 함수를 이용해 삭제되는 기능 구현</li>
</ul>
<p><strong>보완점</strong></p>
<pre><code>const emotionItem= emotionList.find((item)=&gt; String(item.emotionId)===String(emotionId));</code></pre><ul>
<li>리스트에서 굳이 맵이나 필터를 사용하지말고 <code>find</code> 함수를 이용하여 찾자</li>
</ul>
<pre><code>const useDiary=(id)=&gt;{
    const nav=useNavigate();
    const data= useContext(DiaryStateContext);
    const [curDiaryItem,setCurDiaryItem] = useState();
    useEffect(()=&gt;{
        const currentDiaryItem= data.find((item)=&gt; String(item.id)===String(id));
        if(!currentDiaryItem){
            window.alert(&#39;존재하지않는 일기입니다&#39;);
            nav(&#39;/&#39;,{replace:true});
        }
        setCurDiaryItem(currentDiaryItem);
    },[id]);

    return curDiaryItem;
}

export default useDiary;</code></pre><ul>
<li>별도의 후크파일을 만들어 만약 찾고자 하는 일기장 페이지가 없으면 미리 생성해 놓은 후크를 이용하여 <code>null</code> 데이터 처리에 신경쓰자</li>
</ul>
<h2 id="view-component">View Component</h2>
<pre><code>const View=({emotionId,dataR})=&gt;{
    const list = emotionList;
    const data = list.filter((item)=&gt; String(item.emotionId)===String(emotionId));
    const emotionName = data[0].emotionName;
    const date=getStringedDate(dataR[0].createdDate);
    console.log(dataR[0].createdDate);
    return(
        &lt;div&gt;
            &lt;section&gt;
                &lt;h4&gt;오늘의 날짜&lt;/h4&gt;
                &lt;input type=&quot;date&quot; name=&quot;createdDate&quot; id=&quot;&quot; value={date} readOnly/&gt;
            &lt;/section&gt;
            &lt;EmotionItem emotionId={emotionId} emotionName={emotionName}&gt;&lt;/EmotionItem&gt;
            &lt;section&gt;
                &lt;textarea name=&quot;&quot; id=&quot;&quot; value={dataR[0].content} readOnly&gt;&lt;/textarea&gt;
            &lt;/section&gt;

        &lt;/div&gt;
    )
}

export default View</code></pre><ul>
<li>찾은 데이터의 정보를 뿌리는 역할을 맡은 <code>View</code> 컴포넌트이다</li>
<li>주의점은 <code>input</code>,<code>textarea</code>에서 <code>readonly</code>속성을 넣지 않으면 계속 오류가 뜬다</li>
<li>이 이유는 이전까지는 온 체인지 핸들러로 처리하거나 값을 입력받았는데 지금은 값입력을 받으면 안되기 때문이다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프록시와 연관관계]]></title>
            <link>https://velog.io/@java_dong/%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@java_dong/%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84</guid>
            <pubDate>Sun, 09 Mar 2025 06:45:08 GMT</pubDate>
            <description><![CDATA[<h1 id="프록시">프록시</h1>
<p><strong>프록시 기초</strong></p>
<ul>
<li><p><code>em,getReference(Member.class,memberId)</code>: 데이터베이스 조회를 미루는 가짜 엔터티 객체 조회</p>
</li>
<li><p>특징</p>
<ul>
<li>실제 클래스를 상속 받아서 만들어짐</li>
<li>실제 클래스와 모습이 똑같음</li>
<li>이론상 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨</li>
<li>프록시 객체는 진짜 객체의 참조값을 보관</li>
<li>프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드 호출</li>
<li>프록시 객체는 처음 사용할 때 한번만 초기화</li>
<li>초기화할때 프록시 객체가 실제 엔터티로 바뀌는 것은 아님, 초기화 되면 프록시 객체를 통해 실제 엔터티에 접근 가능</li>
<li>프록시 객체는 원본 엔터티를 상속 받음, 타입 체크시 == 비교가 아닌 instance of로 해야함</li>
<li>영속성 컨텍스트에 찾는 엔터티가 이미 존재하면 em.getReference()를 호출해도 실제 엔터티 반환</li>
</ul>
</li>
</ul>
<p><br><br></p>
<h1 id="지연즉시-로딩">지연/즉시 로딩</h1>
<p>*<em>지연로딩 LAZY *</em></p>
<pre><code> @ManyToOne(fetch = FetchType.LAZY) // 이 방법은 프록시객체로 조회히한다
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private Team team;</code></pre><ul>
<li><code>Team</code> 객체를 <code>LAZY</code> 전략으로 조회</li>
</ul>
<pre><code>select
        m1_0.id,
        m1_0.city,
        m1_0.street,
        m1_0.zipcode,
        m1_0.EMP_START,
        m1_0.WORK_STREET,
        m1_0.WORK_ZIPCODE,
        m1_0.TEAM_ID,
        m1_0.username 
    from
        Member m1_0 
    where
        m1_0.id=?</code></pre><ul>
<li><code>Member</code> 객체만 조회하면 <code>Member</code> 테이블만 조회함</li>
<li>지연로딩을 사용해서 <code>Team</code>객체를 프록시로 조회함</li>
</ul>
<pre><code>Member member3= em.find(Member.class, member.getId());
Team team2= member3.getTeam();
System.out.println(team2.getName());

select
        t1_0.TEAM_ID,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.TEAM_ID=?</code></pre><ul>
<li><code>Team</code>의 메서드를 호출하니 그때서야 <code>Team</code> 테이블에서 값 조회</li>
<li><code>Member</code>와 <code>Team</code>을 함께 자주 사용하면 즉시 로딩 사용</li>
</ul>
<pre><code> @ManyToOne(fetch = FetchType.EAGER) 
 @JoinColumn(name = &quot;TEAM_ID&quot;)
 private Team team;</code></pre><ul>
<li>조인 쿼리문이 나가며 동시에 조회됨</li>
</ul>
<pre><code>    select
        m1_0.id,
        m1_0.city,
        m1_0.street,
        m1_0.zipcode,
        m1_0.EMP_START,
        m1_0.WORK_STREET,
        m1_0.WORK_ZIPCODE,
        t1_0.TEAM_ID,
        t1_0.name,
        m1_0.username 
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.TEAM_ID 
    where
        m1_0.id=?
</code></pre><ul>
<li>하지만 가급적이면 지연로딩을 사용하는 것이 좋다</li>
<li>즉시 로딩은 JPQL에서 N+1 문제를 일으킨다</li>
<li><code>@ManyToOne,@OneToMany</code> 는 기본이 즉시 로딩이므로 <code>LAZY</code>로 변경해야함</li>
</ul>
<p><br><br></p>
<h1 id="영속성-전이--cascade">영속성 전이 : CASCADE</h1>
<ul>
<li>특정 엔터테를 영속 상태로 만들 때 연관된 엔터티도 함꼐 영속상태로 만들고 싶을 때 사용</li>
<li>부모엔터티를 저장할 때 자식 엔터티도 함께 저장하는 예시가 있다</li>
</ul>
<pre><code> @OneToMany(mappedBy = &quot;parent&quot;,cascade = CascadeType.PERSIST,orphanRemoval = true)
    private List&lt;Child&gt; childList = new ArrayList&lt;&gt;();

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }</code></pre><p><strong>부모만 저장함에도 자식도 persist 됨</strong></p>
<pre><code>  Parent parent = new Parent();
  parent.setName(&quot;Jack&quot;);
 Child child = new Child();
 child.setName(&quot;Jackson&quot;);
 parent.addChild(child);
em.persist(parent);</code></pre><ul>
<li>영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다</li>
<li>엔터티를 영속화 할떄 연관된 엔터티도 함께 영속화 하는 편리함을 제공할 뿐</li>
<li>영속성 전이 종류<ul>
<li>ALL: 모두 적용</li>
<li>PERSIST: 영속</li>
<li>REMOVE: 삭제</li>
<li>MERGE: 병합</li>
<li>REFRESH</li>
<li>DETACH</li>
</ul>
</li>
</ul>
<p><br><br></p>
<h1 id="고아-객체">고아 객체</h1>
<ul>
<li>고아 객체 제거: 부모 엔터티와 연관관계가 끊어진 자식엔터티를 자동으로 삭제</li>
<li><code>orphanRemoval=true</code></li>
</ul>
<pre><code> em.persist(parent);
 Parent find= em.find(Parent.class, parent.getId());
find.getChildList().removeAll(parent.getChildList());</code></pre><p>** 발생 쿼리 **</p>
<pre><code>Hibernate: 
    /* delete for hellojpa.Child */delete 
    from
        Child 
    where
        id=?</code></pre><ul>
<li>자식객체 삭제됨</li>
</ul>
<p><strong>주의</strong></p>
<ul>
<li>참조가 제거된 엔터티는 다른 곳에서 참조하지않는 고아객체로 보고 삭제하는 기능</li>
<li>참조하는 곳이 하나일때 사용해야함</li>
<li>특정 엔터티가 개인 소유일때 사용</li>
<li><code>@OneToMany</code>,<code>@OneToOne</code> 만 가능</li>
</ul>
<p><br><br></p>
<h1 id="값-타입">값 타입</h1>
<h2 id="기본값-타입">기본값 타입</h2>
<ul>
<li>엔터티 타입<ul>
<li><code>@Entity</code>로 정의하는 객체</li>
<li>데이터가 변해도 식별자로 지속해서 추적 가능</li>
</ul>
</li>
<li>값 타입<ul>
<li>int,Integer,String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체</li>
<li>식별자가 없고 값만 있어 변경시 추적 불가</li>
</ul>
</li>
</ul>
<h2 id="값-타입-분류">값 타입 분류</h2>
<ul>
<li>기본값 타입<ul>
<li>자바 기본 타입</li>
<li>래퍼 클래스</li>
<li>String</li>
</ul>
</li>
<li>임베디드 타입(복합값 타입)</li>
<li>컬렉션 값 타입</li>
</ul>
<h2 id="임베디드-타입">임베디드 타입</h2>
<ul>
<li>새로운 값 타입을 직접 정의 가능</li>
<li>JPA는 임베디드 타입이라함</li>
<li>주로 기본값 타입을 모아서 만들어서 복합값 타입이라고도 함</li>
<li>int, String과 같은 값 타입</li>
</ul>
<p><img src="https://velog.velcdn.com/images/java_dong/post/383e8091-e203-431e-977f-5cb7fb5ac6c3/image.png" alt=""></p>
<ul>
<li>사용법<ul>
<li><code>@Embeddable:</code> 값 타입을 정의하는 곳에 표시</li>
<li><code>@Embedded:</code> 값 타입을 사용하는 곳에 표시</li>
<li>기본 생성자 필수</li>
</ul>
</li>
</ul>
<br>

<p>-장점</p>
<ul>
<li>재사용</li>
<li>높은 응집도</li>
<li><code>Period.isWork()</code>처럼 해당 값 타입만 사용하는 의미있는 메서드를 만들 수 있음</li>
<li>임베디드 타입을 포함한 모든 값 타임은 값타입을 소유한 엔터티의 생명 주기에 의존</li>
</ul>
<br>

<ul>
<li>임베디드 타입은 엔터티의 값일 뿐이다</li>
<li>임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같음</li>
</ul>
<p><br><br></p>
<h2 id="attributeoverride--속성-재정의">@AttributeOverride : 속성 재정의</h2>
<ul>
<li>한 엔터티에서 값은 값 타입을 사용하면 컬럼 명이 중복됨</li>
<li><code>@AttributeOverrides</code>,<code>@AttributeOverride</code>를 사용해서 컬럼명 속성을 재정의</li>
</ul>
<pre><code>@Embeddable
@Setter
@Getter
@NoArgsConstructor
public class Address {
    private String city;
    private String street;
    private String zipcode;
}


@Embedded
@AttributeOverrides({
        @AttributeOverride(name = &quot;city&quot;, column = @Column(name = &quot;EMP_START&quot;)),
        @AttributeOverride(name = &quot;street&quot;, column = @Column(name = &quot;WORK_STREET&quot;)),
        @AttributeOverride(name = &quot;zipcode&quot;, column = @Column(name = &quot;WORK_ZIPCODE&quot;))
})
private Address homeAddress;</code></pre><ul>
<li>이런 값타입의 문제는 바로 객체 타입이라는 것이다</li>
<li>이로 인해 언제든 누군가가 이 객체 타입을 수정할 수 있어 개발자들은 항상 알수없는 위협에 노출되어있다</li>
<li>이 문제를 해결하기 위해서는 <code>setter</code>를 제거해야한다</li>
<li>우리는 값 타입을 불변 타입으로 만들기 위해 노력해야함</li>
<li><code>Embeded</code>로 받은 것은 객체이므로 <code>equals</code>로 동등성을 알아내야함</li>
</ul>
<p><strong>주의</strong></p>
<ul>
<li>값 타입은 정말 값 타입이라 판단될 때만 사용</li>
<li>엔터티와 값 타입을 혼동해서 엔터티를 값 타입으로 만들면 안됨</li>
<li>식별자가 필요하고 지속해서 값을 추적/변경해야 한다면 그것은 값 타입이 아닌 엔터티로 만들어야한다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트로 정리하는 리액트(2)]]></title>
            <link>https://velog.io/@java_dong/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-%EB%A6%AC%EC%95%A1%ED%8A%B82</link>
            <guid>https://velog.io/@java_dong/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-%EB%A6%AC%EC%95%A1%ED%8A%B82</guid>
            <pubDate>Sun, 09 Mar 2025 01:44:20 GMT</pubDate>
            <description><![CDATA[<h1 id="new-새로운-일기-작성-페이지">&#39;/new&#39; 새로운 일기 작성 페이지</h1>
<p><strong>New 페이지</strong></p>
<ul>
<li><code>/new</code> url이 전달되면 해당 페이지로 이동</li>
</ul>
<pre><code>const New =()=&gt;{
    const {onCreate} = useContext(DiaryDispatchContext);
    const nav = useNavigate();
    const onSubmit=(input)=&gt;{
        onCreate(input.createdDate.getTime(),input.emotionId,input.content);
    }
    return(
        &lt;div&gt;
            &lt;Header title={&#39;새 일기 쓰기&#39;} leftChild={&lt;Button text={&quot;&lt; 뒤로 가기&quot;} onClick={()=&gt;{
                nav(-1);
            }}&gt;&lt;/Button&gt;}&gt;&lt;/Header&gt;
            &lt;Editor onSubmit={onSubmit}&gt;&lt;/Editor&gt;
        &lt;/div&gt;
    )

}
</code></pre><ul>
<li><code>Header</code>,<code>Editor</code> 컴포넌트로 url 호출 후 새로운 일기장을 작성하기 위한 페이지 만듦</li>
<li>해당 페이지에서는 <code>Header</code>를 사용해 이전페이지로 이동할 수 있게 <code>child</code>에 <code>Button</code>을 넣음</li>
</ul>
<p><strong>Editor</strong></p>
<pre><code>const emotionList=[
    {
        emotionId:1,
        emotionName:&#39;완전좋음&#39;
    },
    {
        emotionId:2,
        emotionName:&#39;좋음&#39;
    },
    {
        emotionId:3,
        emotionName:&#39;그럭저럭&#39; 
    },
    {
        emotionId:4,
        emotionName:&#39;나쁨&#39;
    },
    {
        emotionId:5,
        emotionName:&#39;끔찍&#39;
    }
];
const getStringedDate=(targetDate)=&gt;{
    let year= targetDate.getFullYear();
    let month= targetDate.getMonth()+1;
    let date= targetDate.getDate();
    if(month&lt;10){
        month=`0${month}`;
    }
    if(date&lt;10){
        date=`0${date}`;
    }
    return `${year}-${month}-${date}`;
}
const Editor=({onSubmit})=&gt;{
    const nav=useNavigate();
    const[input,setInput]=useState({
        createdDate:new Date(),
        emotionId:3,
        content:&quot;&quot;
    });

    const onClickSubmit=()=&gt;{
        onSubmit(input);
    }

    const onChangeInput=(e)=&gt;{
        let name=e.target.name;
        let value=e.target.value;
        if(name===&#39;createdDate&#39;){
            value= new Date(value);
        }
        setInput({...input,[name]:value});

    }

    return(
        &lt;div&gt;
            &lt;section&gt;
                &lt;h4&gt;오늘의 날짜&lt;/h4&gt;
                &lt;input type=&quot;date&quot; name=&quot;createdDate&quot; id=&quot;&quot; value={getStringedDate(input.createdDate)} onChange={onChangeInput}/&gt;
            &lt;/section&gt;
            &lt;section&gt;
                &lt;h4&gt;오늘의 감정&lt;/h4&gt;
                &lt;div&gt;
                    {emotionList.map((item)=&gt; &lt;EmotionItem key={item.emotionId}{...item} isSelected={item.emotionId===input.emotionId} onClick={()=&gt;onChangeInput({
                        target:{
                            name:&#39;emotionId&#39;,
                            value:item.emotionId
                        }
                    })}&gt;&lt;/EmotionItem&gt; )}
                &lt;/div&gt;
            &lt;/section&gt;
            &lt;section&gt;
                &lt;h4&gt;오늘의 일기&lt;/h4&gt;
                &lt;textarea name=&quot;content&quot; id=&quot;&quot; value={input.content} onChange={onChangeInput} placeholder=&quot;오늘은 어땟나요? &quot;&gt;&lt;/textarea&gt;
            &lt;/section&gt;
            &lt;section&gt;
                &lt;Button text={&#39;취소하기&#39;} onClick={()=&gt;{
                    nav(-1);
                }}&gt;&lt;/Button&gt;
                &lt;Button onClick={()=&gt;{
                    onClickSubmit();
                    nav(-1,{replace:true})
                }} text={&#39;작성완료&#39;}&gt;&lt;/Button&gt;
            &lt;/section&gt;
        &lt;/div&gt;
    )
}

export default Editor</code></pre><ul>
<li><code>getStringedData</code>:  <code>input</code>값을 변환해서 <code>xxxx-xx-xx</code> 형식이 되게 만듦</li>
<li><code>onChangeInput</code>: <code>[name]</code>으로 동적으로 객체값을 전달하게 만듦</li>
<li><code>name</code>은 이벤트 객체의 <code>name</code>값을 가지고 오게 함</li>
<li>감정 정보가 담긴 리스트를 <code>map</code>을 사용해 리스트의 숫자만큼 <code>EmotionItem</code>을 반환하게 함</li>
<li>이때 <code>EmotionItem</code>에서는 리스트로 돌고 있는 감정 아이디와 현재 자신이 작성한 일기장의 감정 아이디가 같은지 여부를 전달하는 <code>isSelected</code> 프롭스를 전달받음</li>
<li><code>item</code>을 클릭하면 감정 아이디를 해당 <code>item</code>의 감정 아이디로 변경하는 <code>onClick</code></li>
</ul>
<p><strong>EmotionItem</strong></p>
<pre><code>const EmotionItem=({emotionId,emotionName,isSelected,onClick})=&gt;{
    console.log(isSelected);
    return(
        &lt;div className={`EmotionItem ${isSelected ? `EmotionItem_on_${emotionId}`:&quot;&quot;}` } tabIndex=&quot;0&quot;  onClick={onClick}&gt;
            &lt;img className=&#39;emotion_img&#39; src={getEmotionImage(emotionId)} alt=&quot;&quot; /&gt;
            &lt;div className=&#39;emotion_name&#39;&gt;
                {emotionName}
            &lt;/div&gt;
        &lt;/div&gt;
    )
}</code></pre><ul>
<li>전달 받은 프롭스를 근거로 다른 <code>css</code>를 적용하기 위해 동적으로 <code>className</code> 지정</li>
<li><code>getEmotionImage</code>를 <code>import</code>하여 감정 아이디에 맞는 이미지 호출</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트로 정리하는 리액트 (1)]]></title>
            <link>https://velog.io/@java_dong/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-%EB%A6%AC%EC%95%A1%ED%8A%B8-1</link>
            <guid>https://velog.io/@java_dong/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-%EB%A6%AC%EC%95%A1%ED%8A%B8-1</guid>
            <pubDate>Sat, 08 Mar 2025 08:37:19 GMT</pubDate>
            <description><![CDATA[<h1 id="createupdatedelete">create/Update/Delete</h1>
<pre><code>import { useState,useReducer,useRef } from &#39;react&#39;
import &#39;./App.css&#39;
import { Routes,Route,Link ,useNavigate} from &#39;react-router-dom&#39;
import Home from &#39;./page/Home&#39;

const mockData=[
  {
    id:1,
    createdDate:new Date(&#39;2025-01-08&#39;),
    emotionId:1,
    content:&#39;일번 일기내용&#39;
  },
  {
    id:2,
    createdDate:new Date(&#39;2025-01-18&#39;),
    emotionId:2,
    content:&#39;2번 일기내용&#39;
  },
  {
    id:3,
    createdDate:new Date(&#39;2025-01-28&#39;),
    emotionId:3,
    content:&#39;3번 일기내용&#39;
  },
]
function reducer(state,action){
  switch(action.type){
      case &quot;CREATE&quot; : return [...state,action.data] ;
      case &quot;UPDATE&quot; : return state.map((item)=&gt;String(item.id)===String(action.data.if)? action.data: item);
      case &quot;DELETE&quot; : return state.filter((item)=&gt; String(item.id)!==String(action.data.id));
      default : return state;
  }
}
function App() {
  const idRef=useRef(3);
  const [count, setCount] = useState(0)
  const [data,dispatch] = useReducer(reducer,mockData);
  const onCreate=(createdDate,emotionId,content)=&gt;{
    dispatch({
      type:&quot;CREATE&quot;,
      data:{
        id:idRef.current++,
        createdDate,
        emotionId,
        content
      }
    })
  }

  const onDelete=(id)=&gt;{
    dispatch({
      type:&quot;DELETE&quot;,
      data:{
        id
      }
    })
  }

  const onUpdate=(id,createdDate,content,emotionId)=&gt;{
    dispatch({
      type:&quot;UPDATE&quot;,
      data:{
        id,
        createdDate,
        emotionId,
        content
      }
    })
  }
  return (
    &lt;&gt;
      &lt;Home&gt;&lt;/Home&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre><h1 id="경로-설정하기">경로 설정하기</h1>
<pre><code> return (
    &lt;&gt;
      &lt;DiaryStateContext.Provider value={data}&gt;
        &lt;DiaryDispatchContext.Provider value={{onCreate,onDelete,onUpdate}}&gt;
          &lt;Routes&gt;
              &lt;Route path=&#39;/&#39; element={&lt;Home&gt;&lt;/Home&gt;}&gt;&lt;/Route&gt;
              &lt;Route path=&#39;/new&#39; element={&lt;New&gt;&lt;/New&gt;}&gt;&lt;/Route&gt;
              &lt;Route path=&#39;/diary/:id&#39; element={&lt;Diary&gt;&lt;/Diary&gt;}&gt;&lt;/Route&gt;
              &lt;Route path=&#39;*&#39; element={&lt;NotFound&gt;&lt;/NotFound&gt;}&gt;&lt;/Route&gt;
              &lt;Route path=&#39;/edit/:id&#39; element={&lt;Edit&gt;&lt;/Edit&gt;}&gt;&lt;/Route&gt;
          &lt;/Routes&gt;
        &lt;/DiaryDispatchContext.Provider&gt;
      &lt;/DiaryStateContext.Provider&gt;
    &lt;/&gt;
  )
}</code></pre><ul>
<li><code>*</code> : 지정되지 않은 루트로 이동하면 정상적이지 않은 페이지라고 알려주는 페이지로 이동시킴</li>
<li><code>createContext</code>로 <code>props</code>를 이용하여 부모에서 자식으로 전달이 아닌 어디에서나 접근이 가능하게 만듦</li>
<li><code>/edit:id</code> : <code>/edit/3</code>과 같은 url로 접근하면 해당 아이디가 가지고 있는 정보를 나타내는 페이지로 이동</li>
</ul>
<p><strong>주의</strong></p>
<pre><code> &lt;BrowserRouter&gt;
        &lt;App /&gt;
    &lt;/BrowserRouter&gt;</code></pre><ul>
<li><code>Routes</code>를 쓸때는 <code>App</code>이 <code>BrowserRouter</code>로 감싸져 있어야 함</li>
</ul>
<p><br><br></p>
<h1 id="-home-page-구축">&#39;/&#39; Home page 구축</h1>
<p><strong>HomePage 이미지</strong></p>
<p><img src="https://velog.velcdn.com/images/java_dong/post/74e99225-d3d7-4046-9ad8-a569bb5f24e1/image.png" alt=""></p>
<ul>
<li>이렇게 만들기 위해 <code>components</code> 폴더를 만든 후 <code>Header</code>,<code>Button</code>,<code>DiaryList</code>,<code>DiaryItem</code> 컴포넌트를 구현함</li>
</ul>
<p><strong>Header</strong></p>
<pre><code>const Header=({title,leftChild,rightChild})=&gt;{
    return &lt;header className=&#39;Header&#39;&gt;
            &lt;div className=&#39;header_left&#39;&gt;{leftChild}&lt;/div&gt;
            &lt;div className=&#39;header_center&#39;&gt;{title}&lt;/div&gt;
            &lt;div className=&#39;header_right&#39;&gt;{rightChild}&lt;/div&gt;
        &lt;/header&gt;

}

export default Header
</code></pre><ul>
<li><code>title</code>에는 텍스트 구문 <code>leftChild</code>,<code>rightChild</code>의 경우는 <code>Button</code> 컴포넌트를 상속받음</li>
</ul>
<p><strong>Button</strong></p>
<pre><code>const Button=({text,type,onClick})=&gt;{
    return   &lt;button className={`Button Button_${type}`} onClick={onClick} &gt; {text}&lt;/button&gt;
}

export default Button</code></pre><ul>
<li>버튼의 타입에 따라 다른 <code>css</code>를 적용하기 위해 동적으로 클래스 생성</li>
<li>상속받은 <code>onClick</code> 함수 적용</li>
</ul>
<p><strong>DiaryList</strong></p>
<pre><code>const DiaryList=({data})=&gt;{

    const nav=useNavigate();
    const [sortType,setSortType]=useState(&quot;latest&quot;);
    const onChangeSortType=(e)=&gt;{
        setSortType(e.target.value);
    }
    // 원본배열을 수정하면 문제가 생길수있어 새로운 배열로 반환받음
    const getSortedData=()=&gt;{
        return data.toSorted((a,b)=&gt;{
            if(sortType==&quot;oldest&quot;){
                return Number(a.createdDate) -Number(b.createdDate);
            }else{
                return Number(b.createdDate)-Number(a.createdDate);
            }
        })
    }

    const sortedData =getSortedData();
    return &lt;&gt;

    &lt;div className=&quot;DiaryList&quot;&gt;
        &lt;div className=&quot;menu_bar&quot;&gt;
            &lt;select name=&quot;&quot; id=&quot;&quot; onChange={onChangeSortType}&gt;
                &lt;option value=&quot;latest&quot;&gt;최신순&lt;/option&gt;
                &lt;option value=&quot;oldest&quot;&gt; 오래된순&lt;/option&gt;

            &lt;/select&gt;
            &lt;Button text={&#39;새로운 일기 쓰기&#39;} type={&#39;POSITIVE&#39;} onClick={()=&gt;{
                nav(&quot;/new&quot;)
            }}&gt;&lt;/Button&gt;
        &lt;/div&gt;
        &lt;div className=&quot;list_wrapper&quot;&gt;
            {sortedData.map((item)=&gt;{
                return &lt;DiaryItem key={item.id}{...item}&gt;&lt;/DiaryItem&gt;
            })}
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;/&gt;
}

export default DiaryList</code></pre><ul>
<li>상속받은 데이터를 조작하면 문제가 될 수 있어서 함수로 새 데이터를 반환하게 함</li>
<li>최신순 , 오래된순 을 각각 클릭하면 <code>onChangeSortType</code>에서 <code>setSortType</code>으로 타입 조작과 동시에 <code>getSortedData()</code>를 새로 반환</li>
<li>그렇게 정렬한 데이터를 <code>map</code>을 써서 배열을 순환하며 각 데이터를 <code>DiaryItem</code>으로 <code>props</code>형태로 전달</li>
</ul>
<p><strong>DiaryItem</strong></p>
<pre><code>const DiaryItem=({id,emotionId,createdDate,content})=&gt;{
    const nav=useNavigate();
    return (
        &lt;div className=&quot;DiaryItem&quot;&gt;
            &lt;div className={`img_section img_section_${emotionId}`} onClick={()=&gt;nav(`/diary/${id}`)}&gt;
                &lt;img src={getEmotionImage(emotionId)} alt=&quot;&quot; /&gt;
            &lt;/div&gt;
            &lt;div className=&quot;info_section&quot; onClick={()=&gt;nav(`/diary/${id}`)}&gt;
                &lt;div className=&quot;created_date&quot;&gt;{new Date(createdDate).toLocaleDateString()}&lt;/div&gt;
                &lt;div className=&quot;content&quot;&gt;
                    {content}
                &lt;/div&gt;
            &lt;/div&gt;
            &lt;div className=&quot;button_section&quot;&gt;
                &lt;Button text={&#39;수정하기&#39;} onClick={()=&gt;nav(`/edit/${id}`)}&gt;&lt;/Button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    )

}

export default DiaryItem</code></pre><ul>
<li><code>getEmtionImage():</code> 별도의 자바 스크립트 파일로 만들어 <code>emotionId</code>에 따라 다른 이미지 전달</li>
<li>수정버튼이나 이미지를 클릭하면 수정페이지와 상세보기 페이지로 이동</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[연관관계 매핑]]></title>
            <link>https://velog.io/@java_dong/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@java_dong/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Fri, 07 Mar 2025 08:04:22 GMT</pubDate>
            <description><![CDATA[<h1 id="단방향-연관관계">단방향 연관관계</h1>
<p> <img src="https://velog.velcdn.com/images/java_dong/post/c65d774e-665e-4218-af07-9a7d9fb774f0/image.png" alt=""></p>
<p><strong>객체의 참조와 테이블의 외래키 매핑</strong></p>
<pre><code>@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    private int age;
    @Enumerated(EnumType.STRING)
    private MemberType type;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name=&quot;TEAM_ID&quot;)
    private Team team;

    public void createMember(Team team) {
        this.team = team;
        team.getMembers().add(this);

    }

    @Enumerated(EnumType.STRING)
    private MemberType memberType;
 }</code></pre><p><strong>객체지향 모델링</strong></p>
<pre><code>Team team = new Team(); 
team.setName(&quot;TeamA&quot;); 
em.persist(team); 
 
//회원 저장 
Member member = new Member(); 
member.setName(&quot;member1&quot;); 
member.setTeam(team); //단방향 연관관계 설정, 참조 저장 
em.persist(member); </code></pre><p><strong>객체 그래프 탐색 및 수정</strong></p>
<pre><code>//조회 
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회 
Team findTeam = findMember.getTeam();

// 새로운 팀B 
Team teamB = new Team(); 
teamB.setName(&quot;TeamB&quot;); 
em.persist(teamB); 
 
// 회원1에 새로운 팀B 설정 
member.setTeam(teamB); </code></pre><p><br><br></p>
<h1 id="양방향-연관관계와-연관관계의-주인">양방향 연관관계와 연관관계의 주인</h1>
<p><img src="https://velog.velcdn.com/images/java_dong/post/1acb6c4f-50a7-4a1d-9c18-10ea75721ddf/image.png" alt=""></p>
<p><strong>양방향 매핑(팀 엔터티에 @OneToMany 추가)</strong></p>
<pre><code>@Entity
@Setter
@Getter
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @BatchSize(size = 100)
    @OneToMany(mappedBy = &quot;team&quot;)
    private List&lt;Member&gt;members = new ArrayList&lt;&gt;();
}</code></pre><p><strong>연관관계의 주인</strong></p>
<ul>
<li>객체의 두 관계 중 하나를 연관관계의 주인으로 지정</li>
<li>연관관계의 주인만이 외래키를 관리</li>
<li>주인이 아닌 쪽은 읽기만 가능하고 수정을 불가능</li>
<li>주인이 아니면 <code>mappedBy</code> 속성으로 주인 지정</li>
<li>양방향 연관관계에서 주인은 외래키가 있는 <code>다</code>(여기서는 <code>Member</code>) 쪽에 설정하는 것이 좋다</li>
</ul>
<p><br><br></p>
<h1 id="다대일-단방향-양방향">다대일 단방향/ 양방향</h1>
<p><strong>단방향</strong></p>
<p><img src="https://velog.velcdn.com/images/java_dong/post/59fa04b8-dd6b-417f-9442-c04f31318625/image.png" alt=""></p>
<ul>
<li>가장 많이 사용하는 연관관계</li>
<li>다대일의 반대는 일대다</li>
</ul>
<p><strong>양방향</strong></p>
<p><img src="https://velog.velcdn.com/images/java_dong/post/11e112a4-3c1c-49ed-931d-6f5bb7ca1774/image.png" alt=""></p>
<ul>
<li>외래키가 있는 쪽이 연관관계의 주인</li>
<li>양쪽을 서로 참고하도록 개발</li>
</ul>
<h1 id="일대다-단방향">일대다 단방향</h1>
<p><strong>일대다 단방향</strong></p>
<p><img src="https://velog.velcdn.com/images/java_dong/post/24ce227a-0fad-4817-a4ca-e9623e8741da/image.png" alt=""></p>
<ul>
<li><p>일대다 단방향은 일대다에서 일이 연관관계의 주인</p>
</li>
<li><p>테이블 일대다 관계에서는 항상 다 쪽에 외래키가 있음</p>
</li>
<li><p><code>@JoinColumn</code>을 사용하지 않으면 조인테이블 방식을 사용하여 중간에 테이블을 하나 추가함</p>
</li>
<li><p>단점</p>
<ul>
<li>엔터티가 관리하는 외래키가 다른 테이블에 있음</li>
<li>연관관계 관리를 위해 추가로 UPDATE SQL을 실행</li>
<li>일대다 단방향 보다는 다대일 양방향을 권장</li>
</ul>
</li>
</ul>
<h1 id="일대일-관계">일대일 관계</h1>
<ul>
<li>주 테이블이나 대상 테이블 중에 외래키 선택 가능</li>
<li>다대일 단방향 매핑과 유사</li>
<li>일대일 양방향 매핑에서는 외래키가 있는 곳이 다대일과 마찬가지로 연관관계의 주인이다</li>
<li>반대편은 <code>mappedBy</code> 적용</li>
</ul>
<h1 id="다대다-관계">다대다 관계</h1>
<ul>
<li>무조건 일대다, 다대일 관계로 다시 나눠야함</li>
<li>편리해 보이지만 실무에서 사용 x</li>
</ul>
<p><br><br></p>
<h1 id="상속관계-매핑">상속관계 매핑</h1>
<ul>
<li>관계형 데이터베이스는 상속이란 개념이 없다</li>
<li>객체의 상속과 구조가 DB의 슈퍼타입 서브타입 관계를 매핑<ul>
<li>각각 테이블로 변환 =&gt; 조인 전략</li>
<li>통합 테이블로 변환 =&gt; 단일 테이블 전략</li>
<li>서브타입 테이블로 변환 =&gt; 구현 클래스마다 테이블 전략</li>
</ul>
</li>
</ul>
<p><strong>주요 에노테이션</strong></p>
<ul>
<li><code>@Inheritance(strategy=InheritanceType.xxx)</code><ul>
<li><code>Joined</code>: 조인 전략</li>
<li><code>Single Table</code>: 단일 테이블 전략</li>
<li><code>Table_Per_class</code>: 구현 클래스마다 전략</li>
</ul>
</li>
<li><code>@DiscriminatorColumn(name=&quot;DTYPE&quot;)</code></li>
<li><code>@DiscriminatorValue(&quot;XXX&quot;)</code></li>
</ul>
<p><strong>조인 전략</strong></p>
<p><img src="https://velog.velcdn.com/images/java_dong/post/de800c98-d10d-471b-a78b-4151985f02c1/image.png" alt=""></p>
<ul>
<li>장점<ul>
<li>테이블 정규화</li>
<li>외래 키 참조 무결성 제약조건 활용 가능</li>
<li>저장공간 효율화</li>
</ul>
</li>
<li>단점<ul>
<li>조회시 조인을 너무 많이 사용</li>
<li>조회 쿼리의 복잡도</li>
<li>데이터 저장시 INSERT 쿼리를 두번 호출</li>
</ul>
</li>
</ul>
<p><strong>단일 테이블 전략</strong>
<img src="https://velog.velcdn.com/images/java_dong/post/223fe800-87f6-43e0-9c2c-0df0a2a9fcad/image.png" alt=""></p>
<ul>
<li>장점<ul>
<li>조인이 필요 없으므로 일반적으로 조회 성능이 빠름</li>
<li>조회 쿼리가 단순함</li>
</ul>
</li>
<li>단점<ul>
<li>자식 엔터티가 매핑한 컬럼은 모두 null 허용</li>
<li>단일테이블에 모든 것을 저장하므로 테이블이 커질수 있다 상황에 따라 오히려 느려질 가능성이 있다</li>
</ul>
</li>
</ul>
<p><strong>구현 클래스마다 전략</strong>
<img src="https://velog.velcdn.com/images/java_dong/post/c9eae639-0b90-41a2-b321-e38a5be5c912/image.png" alt=""></p>
<ul>
<li>이 전략은 가급적이면 사용하지 않는것이 좋다</li>
<li>장점<ul>
<li>서브 타입을 명확하게 구분해서 처리할 때 효과적</li>
<li>not null 제약조건 사용 가능</li>
</ul>
</li>
<li>단점<ul>
<li>여러 자식 테이블을 함께 조회할 때 성능이 느림</li>
<li>자식 테이블을 통합해서 쿼리하기 어려움</li>
</ul>
</li>
</ul>
<p><strong>상속 매핑 예시</strong></p>
<pre><code>@Entity
@Setter
@Getter
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn
public abstract class Item {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int price;
}
@Entity
@Setter
@Getter
@ToString
@DiscriminatorValue(&quot;M&quot;)
public class Movie extends Item{
    private String director;
    private String actor;
}</code></pre><p><strong>@MappedSuperclass</strong></p>
<ul>
<li><p>공통 매핑 정보가 필요할 때 사용</p>
</li>
<li><p>테이블과 관계 없고 단군히 엔터티가 공통으로 사용하는 매핑정보를 모으는 역할</p>
</li>
<li><p>주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔터티에서 공통으로 적용하는 정보를 모을 때 사용</p>
</li>
<li><p>사용 예시</p>
</li>
</ul>
<p><strong>BaseEntity Test</strong></p>
<pre><code>@MappedSuperclass
@Setter
@Getter
public class BaseEntity {
    @Column(name=&quot;created_by&quot;)
    private String createdBy;
    private LocalDateTime createdTime;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
}


@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Test extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    @Column
    private String name;


}</code></pre><p><strong>실행 결과 테스트 테이블</strong></p>
<p><img src="https://velog.velcdn.com/images/java_dong/post/45330f62-42a5-4d88-8caf-a17dfd2317c1/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA의 시작]]></title>
            <link>https://velog.io/@java_dong/JPA%EC%9D%98-%EC%8B%9C%EC%9E%91</link>
            <guid>https://velog.io/@java_dong/JPA%EC%9D%98-%EC%8B%9C%EC%9E%91</guid>
            <pubDate>Thu, 06 Mar 2025 05:23:32 GMT</pubDate>
            <description><![CDATA[<h1 id="jpa의-동작">JPA의 동작</h1>
<ul>
<li><p><code>@Entity</code>: JPA가 관리할 객체</p>
</li>
<li><p><code>@Id</code>: 데이터베이스 PK와 매칭</p>
</li>
<li><p>회원 등록, 수정 , 삭제, 수정 ,단건 조회 기능</p>
</li>
</ul>
<p><br><br></p>
<h1 id="jpql">JPQL</h1>
<ul>
<li>JPA를 사용하면 엔터티 객체를 중심으로 개발</li>
<li>검색을 할 때도 테이블이 아닌 엔터티 객체를 대상으로 검색</li>
<li>모든 데이터베이스 데이터를 객체로 변환하여 검색하는 것은 불가능</li>
<li>SQL을 추상화한 JPQL이라는 객체 지향 쿼리언어 제공</li>
<li>JPQL은 엔터티 객체를 대상으로 만들어진 쿼리문</li>
</ul>
<p><br><br></p>
<h1 id="영속성-컨텍스트">영속성 컨텍스트</h1>
<p><strong>영속성 컨텍스트 의미</strong></p>
<ul>
<li>엔터티를 영구 저장하는 환경이라는 뜻</li>
<li><code>EntityManger.persist(entity)</code></li>
<li>영속성 컨텍스트는 논리적인 개념으로 눈에 보이지 x</li>
<li>엔터티 매니저를 통해서 영속성 컨텍스트에 접근</li>
</ul>
<p><strong>엔터티의 생명주기</strong></p>
<ul>
<li>비영속: 영속성 컨텍스트와 전혀 관계없는 새로운 상태<ul>
<li>객체를 막 생성한 상태는 비영속 상태</li>
</ul>
</li>
<li>영속: 영속성 컨텍스트에 의해 관리되는 상태<ul>
<li>객체를 생성후 저장하면 영속상태</li>
</ul>
</li>
<li>준영속: 영속성 컨텍스트에 저장되었다가 분리된 상태</li>
<li>삭제</li>
</ul>
<p><strong>영속성 컨텍스트 장점</strong></p>
<ul>
<li>1차 캐시를 이용<ul>
<li>객체를 영속상태로 만들면 이후 1차 캐시에서 관리 =&gt; <code>em.find(Member.class,&quot;member1&quot;)</code>을 실행하면 데이터베이스가 아닌 1차 캐시에서 데이터를 탐색</li>
<li>만약 1차 캐시에 없으면 데이터베이스에서 해당 내용을 찾은 후 그 내용을 1차 캐시에 저장</li>
</ul>
</li>
<li>동일성 보장<ul>
<li><code>==</code> 으로 비교해도 <code>true</code> 값이 나와 동일성을 보장</li>
</ul>
</li>
<li>트랜잭션을 지원하는 쓰기 지연<ul>
<li><code>em.persist(member)</code> 까지는 INSERT SQL을 데이터베이스에 보내지 않음</li>
<li>커밋하는 순간 데이터베이스에 INSERT SQL 전송</li>
</ul>
</li>
<li>변경감지<ul>
<li>영속 컨텍스트 조회 후 <code>Update</code>라는 쿼리문을 사용하지 않고 영속 엔터티를 수정해도 변경을 감지하여 어플리케이션 내부에서 <code>Update</code> 구문이 전송됨</li>
</ul>
</li>
<li>지연로딩</li>
</ul>
<p><strong>플러시</strong></p>
<ul>
<li>영속성 컨텍스트의 변경 내용을 데이터베이스에 반영</li>
<li>쓰기지연 SQL 저장소의 쿼리를 데이터베이스에 전송</li>
<li><code>em.flush(), transaction.commit(), JPQL 쿼리 실행</code>을 하면 플러시가 호출</li>
<li>플러시는 영속성 컨텍스트를 비우는 것이 아닌 영속성 컨텍스트의 변경사항을 데이터베이스에 동기화 하는 것</li>
</ul>
<p><strong>준영속 상태로 만드는 법</strong></p>
<ul>
<li><code>em.detach(entity)</code></li>
<li><code>em.clear()</code></li>
<li><code>em.close()</code></li>
</ul>
<p><br><br></p>
<h1 id="엔터티-매핑">엔터티 매핑</h1>
<p>** @Entity**</p>
<ul>
<li>name <ul>
<li>JPA에서 사용할 엔터티 이름을 지정</li>
<li>기본값: 클래스의 이름을 그대로 사용</li>
</ul>
</li>
</ul>
<p><strong>@Table</strong></p>
<ul>
<li>name : 매핑할 테이블 이름</li>
<li>schema: 데이터베이스 schema 매핑<ul>
<li>create:기존 테이블 삭제후 다시 생성</li>
<li>create-drop: create와 같으니 종료시점에 테이블 DROP</li>
<li>update: 변경분만 반영</li>
<li>validate: 엔터티와 테이블이 정상 매핑되었는지 확인</li>
<li>none: 사용하지 않음</li>
</ul>
</li>
<li>unique constraints: DDL 생성시에 유니크 제약조건 생성<ul>
<li><code>@Column(nullable=false,length=10)</code></li>
</ul>
</li>
</ul>
<p><strong>필드와 컬럼 매핑</strong></p>
<pre><code>@Entity
public class Member {
    @Id
    private Long id;

    @Column(name = &quot;name&quot;)
    private String username;

    private Integer age;

    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;

    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;

    @Lob
    private String description;
}</code></pre><ul>
<li><code>@Column</code> : 컬럼매핑</li>
<li><code>@Temporal</code>: 날짜타입 매핑</li>
<li><code>@Enumerated</code>: enum 타입 매핑</li>
<li><code>@Lob</code>: BLOB,CLOB 매핑</li>
<li><code>@Transient</code>: 특정 필드를 컬럼에 매핑 x</li>
</ul>
<p><strong>@Column</strong></p>
<ul>
<li>name: 필드와 매핑할 테이블의 컬럼이름</li>
<li>insertable/updateable: 등록 변경 가능 여부 (기본값:True)</li>
<li>nullable: null값의 허용여부</li>
<li>unique: 컬럼에 간단히 유니크 제약조건을 걸 때 사용</li>
<li>length: String 에서만 사용</li>
</ul>
<p><strong>@Enumerated</strong></p>
<ul>
<li>EnumType.ORDINAL: enum의 순서를 데이터베이스에 저장</li>
<li>EnumType.STRING: enum 이름을 데이터베이스에 저장</li>
</ul>
<p><strong>@Temporal</strong></p>
<ul>
<li>날짜 타입을 매핑할 때 사용</li>
<li>LocalDate,LocalDateTime을 사용할 때는 생략 가능</li>
</ul>
<p><br><br></p>
<h1 id="기본키-매핑">기본키 매핑</h1>
<p><strong>기본키 매핑 방법</strong></p>
<ul>
<li>직접 할당: <code>@Id</code> 사용</li>
<li>자동생성(<code>@GeneratedValue</code>)<ul>
<li><code>IDENTITY</code>: 데이터베이스에 위임</li>
<li><code>SEQUENCE</code>: 데이터베이스 시퀀스 오브젝트 사용</li>
<li><code>AUTO</code>: 방언에 따라 자동 지정</li>
</ul>
</li>
</ul>
<p><strong>IDENTITY 전략</strong></p>
<ul>
<li>기본키 생성을 데이터베이스에 위임</li>
<li>AUTO_INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있음</li>
<li>IDENTITY 전략은 <code>em.persist()</code> 시점에 즉시 INSERT SQL을 실행하고 DB에서 식별자를 조회</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터 접근 기술 _Querydsl]]></title>
            <link>https://velog.io/@java_dong/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%91%EA%B7%BC-%EA%B8%B0%EC%88%A0-Querydsl</link>
            <guid>https://velog.io/@java_dong/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%91%EA%B7%BC-%EA%B8%B0%EC%88%A0-Querydsl</guid>
            <pubDate>Thu, 27 Feb 2025 12:10:35 GMT</pubDate>
            <description><![CDATA[<h1 id="querydsl-적용">Querydsl 적용</h1>
<pre><code>@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {

    private final EntityManager em;
    private final JPAQueryFactory query;

    public JpaItemRepositoryV3(EntityManager em) {
        this.em = em;
        this.query = new JPAQueryFactory(em);
    }

    @Override
    public Item save(Item item) {
        em.persist(item);
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findItem = em.find(Item.class, itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    @Override
    public Optional&lt;Item&gt; findById(Long id) {
        Item item = em.find(Item.class, id);
        return Optional.ofNullable(item);
    }

    public List&lt;Item&gt; findAllOld(ItemSearchCond cond) {

        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        QItem item = QItem.item;
        BooleanBuilder builder = new BooleanBuilder();
        if (StringUtils.hasText(itemName)) {
            builder.and(item.itemName.like(&quot;%&quot; + itemName + &quot;%&quot;));
        }
        if (maxPrice != null) {
            builder.and(item.price.loe(maxPrice));
        }

        List&lt;Item&gt; result = query
                .select(item)
                .from(item)
                .where(builder)
                .fetch();

        return result;
    }

    @Override
    public List&lt;Item&gt; findAll(ItemSearchCond cond) {

        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        return query
                .select(item)
                .from(item)
                .where(likeItemName(itemName), maxPrice(maxPrice))
                .fetch();
    }

    private BooleanExpression likeItemName(String itemName) {
        if (StringUtils.hasText(itemName)) {
            return item.itemName.like(&quot;%&quot; + itemName + &quot;%&quot;);
        }
        return null;
    }

    private BooleanExpression maxPrice(Integer maxPrice) {
        if (maxPrice != null) {
            return item.price.loe(maxPrice);
        }
        return null;
    }
}
</code></pre><ul>
<li>Querydsl을 사용하려면 <code>JpaQueryFactory</code>가 필요하다</li>
<li>이때 <code>JpaQueryFaactory</code>믐 Jpa 쿼리인 JPQL을 만들기 때문에 <code>EntityManager</code>가 필요</li>
<li><code>JpaQueryFactory</code>를 빈으로 등록해서 사용 가능</li>
</ul>
<p><strong>findAll()</strong></p>
<pre><code> @Override
    public List&lt;Item&gt; findAll(ItemSearchCond cond) {

        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        return query
                .select(item)
                .from(item)
                .where(likeItemName(itemName), maxPrice(maxPrice))
                .fetch();
    }</code></pre><ul>
<li><p><code>QueryDsl</code>에서 <code>where(A,B)</code>에 다양한 조건들을 직접 넣을 수 있다. 이렇게 넣으면 AND조건으로 처리되며 <code>where()</code>에 null값을 넣으면 해당 조건은 무시됨</p>
</li>
<li><p>코드의 재사용성이 높아져 다른 쿼리를 작성할 때 <code>likeItemName</code> , <code>maxPrice</code> 를 이용 할 수 있다</p>
</li>
</ul>
<p><br><br><br><br>
출처:<a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터 접근 기술 _ 스프링 JPA]]></title>
            <link>https://velog.io/@java_dong/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%91%EA%B7%BC-%EA%B8%B0%EC%88%A0-%EC%8A%A4%ED%94%84%EB%A7%81-JPA</link>
            <guid>https://velog.io/@java_dong/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%91%EA%B7%BC-%EA%B8%B0%EC%88%A0-%EC%8A%A4%ED%94%84%EB%A7%81-JPA</guid>
            <pubDate>Wed, 26 Feb 2025 11:26:43 GMT</pubDate>
            <description><![CDATA[<p><strong>스프링 데이터 JPA 주요기능</strong></p>
<ul>
<li>공통 인터페이스 기능<ul>
<li><code>JpaRepository</code> 인터페이스를 통해 기본적인 CRUD 기능 제공</li>
<li>공통화 가능한 기능이 거의 모두 포함</li>
</ul>
</li>
<li>쿼리 메서드 기능</li>
</ul>
<p><strong>JPARepository 사용법</strong></p>
<pre><code> public interface ItemRepository extends JpaRepository&lt;Item, Long&gt; {
 }</code></pre><ul>
<li><code>JpaRepository</code> 인터페이스를 인터페이스 상속 받고, 제네릭에 관리할 <code>&lt;엔티티, 엔티티ID&gt;</code> 를 주면 된다</li>
<li><code>JpaRepository</code>가 제공하는 기본 CRUD기능을 모두 사용 가능</li>
<li><code>JpaRepository</code>만 상속받으면 스ㅡㅍ링 데이터 JPA가 프록시 기술을 사용해서 구현 클래스를 만들어 준다. 그리고 만든 구현 클래스의 인스턴스를 만들어서 스프링 빈으로 등록</li>
</ul>
<p><strong>쿼리메서드 기능</strong></p>
<pre><code> public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
     List&lt;Member&gt; findByUsernameAndAgeGreaterThan(String username, int age);
}</code></pre><ul>
<li>스프링 데이터 JPA는 메서드 이름을 분석해서 필요한 JPQL을 만들고 실행해 준다.</li>
</ul>
<p><strong>JPQL 사용 예시</strong></p>
<pre><code>public interface SpringDataJpaItemRepository extends JpaRepository&lt;Item, Long&gt; {

    List&lt;Item&gt; findByItemNameLike(String itemName);

    List&lt;Item&gt; findByPriceLessThanEqual(Integer price);

    //쿼리 메서드 (아래 메서드와 같은 기능 수행)
    List&lt;Item&gt; findByItemNameLikeAndPriceLessThanEqual(String itemName, Integer price);

    //쿼리 직접 실행
    @Query(&quot;select i from Item i where i.itemName like :itemName and i.price &lt;= :price&quot;)
    List&lt;Item&gt; findItems(@Param(&quot;itemName&quot;) String itemName, @Param(&quot;price&quot;) Integer price);
}
</code></pre><ul>
<li>쿼리메서드 기능 대신에 직접 JPQL을 사용하고 싶을 때는 <code>@Query</code>와 함께 JPQL을 작성하면 도니다. 이때 메서드 이름으로 실행되는 규칙은 무시</li>
</ul>
<p><br><br></p>
<h1 id="스프링-데이터-jpa-적용1">스프링 데이터 JPA 적용1</h1>
<pre><code>List&lt;Item&gt; findByItemNameLike(String itemName);

List&lt;Item&gt; findByPriceLessThanEqual(Integer price);

    //쿼리 메서드 (아래 메서드와 같은 기능 수행)
List&lt;Item&gt; findByItemNameLikeAndPriceLessThanEqual(String itemName, Integer price);

    //쿼리 직접 실행
@Query(&quot;select i from Item i where i.itemName like :itemName and i.price &lt;= :price&quot;)
List&lt;Item&gt; findItems(@Param(&quot;itemName&quot;) String itemName, @Param(&quot;price&quot;) Integer price);</code></pre><p><strong>findAll()</strong></p>
<ul>
<li>모든 아이템을 조회</li>
<li><code>select i from Item i</code></li>
</ul>
<p><strong>findByItemNameLike()</strong></p>
<ul>
<li>이름조건만 검색했을 때 사용하는 쿼리</li>
<li><code>select i from Item i where i.name like ?</code></li>
<li><em>findByPriceLessThanEqual()*</em></li>
<li>가격조건만 검색했을 때 사용하는 쿼리</li>
<li><code>select i from Item i where i.price &lt;= ?</code></li>
</ul>
<p><strong>findByItemNameLikeAndPriceLessThanEqual()</strong></p>
<ul>
<li>이름과 가격조건을 검색했을 때 사용하는 메서드</li>
<li><code>select i from Item i where i.name like ? and i.price &lt;= ?</code></li>
</ul>
<p>쿼리를 직접 사용하려면 <code>@Query</code> 구문을 쓰면된다. 변수를 바인딩 할 때는 <code>@Param(&quot;itemName&quot;)</code>
와 같이 지정하면 됨</p>
<p><br><br></p>
<h1 id="스프링-데이터-jpa-적용-2">스프링 데이터 JPA 적용 2</h1>
<pre><code>@Repository
@Transactional
@RequiredArgsConstructor
public class JpaItemRepositoryV2 implements ItemRepository {

    private final SpringDataJpaItemRepository repository;

    @Override
    public Item save(Item item) {
        return repository.save(item);
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findItem = repository.findById(itemId).orElseThrow();
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    @Override
    public Optional&lt;Item&gt; findById(Long id) {
        return repository.findById(id);
    }

    @Override
    public List&lt;Item&gt; findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        if (StringUtils.hasText(itemName) &amp;&amp; maxPrice != null) {
//            return repository
            .findByItemNameLikeAndPriceLessThanEqual(&quot;%&quot; + itemName + &quot;%&quot;, maxPrice);
            return repository.findItems(&quot;%&quot; + itemName + &quot;%&quot;, maxPrice);
        } else if (StringUtils.hasText(itemName)) {
            return repository.findByItemNameLike(&quot;%&quot; + itemName + &quot;%&quot;);
        } else if (maxPrice != null) {
            return repository.findByPriceLessThanEqual(maxPrice);
        } else {
            return repository.findAll();
        }
    }
}</code></pre><p><strong>의존관계와 구조</strong></p>
<ul>
<li><code>ItemService</code>는 <code>ItemRepository</code>에 의존하므로 <code>ItemService</code>에서 직접 <code>SpringDataJpaItemRepository</code>을 사용할 수 없음</li>
<li>이를 해결하기 위해 <code>JpaItemRepository2</code>를 <code>ItemRepository</code> 와 <code>SpringDataJpaItemRepository</code> 사이를 맞추 기 위한 어댑터 처럼 사용함</li>
</ul>
<p>** findAll()**</p>
<ul>
<li>모든 데이터조회</li>
<li>이름 조회</li>
<li>가격 조회</li>
<li>이름 + 기격 조회</li>
</ul>
<pre><code>    @Override
    public List&lt;Item&gt; findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        if (StringUtils.hasText(itemName) &amp;&amp; maxPrice != null) {
//            return repository
            .findByItemNameLikeAndPriceLessThanEqual(&quot;%&quot; + itemName + &quot;%&quot;, maxPrice);
            return repository.findItems(&quot;%&quot; + itemName + &quot;%&quot;, maxPrice);
        } else if (StringUtils.hasText(itemName)) {
            return repository.findByItemNameLike(&quot;%&quot; + itemName + &quot;%&quot;);
        } else if (maxPrice != null) {
            return repository.findByPriceLessThanEqual(maxPrice);
        } else {
            return repository.findAll();
        }
    }
}</code></pre><ul>
<li><code>like</code> =&gt; <code>% + itemName + %</code>  이 부분을 주의하자</li>
<li>그냥 <code>itemName</code>이름을 넣으면 x</li>
</ul>
<p><br><br><br><br>
출처:<a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터 접근 기술 _ JPA ]]></title>
            <link>https://velog.io/@java_dong/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%91%EA%B7%BC-%EA%B8%B0%EC%88%A0-JPA</link>
            <guid>https://velog.io/@java_dong/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%91%EA%B7%BC-%EA%B8%B0%EC%88%A0-JPA</guid>
            <pubDate>Tue, 25 Feb 2025 09:57:09 GMT</pubDate>
            <description><![CDATA[<h1 id="jpa-시작">JPA 시작</h1>
<p>JPA는 ORM 데이터 접근 기술을 제공</p>
<p> <strong>JPA 설정</strong></p>
<p> <code>spring-boot-starter-data-jpa</code> 라이브러리를 사용하면 JPA와 스프링 데이터 JPA를 스프링 부트와 통합하고 설정도 간단히 할 수 있다</p>
<p> <strong>application.properties</strong></p>
<pre><code> logging.level.org.hibernate.SQL=DEBUG
 logging.level.org.hibernate.orm.jdbc.bind=TRACE</code></pre><ul>
<li><p>다음과 같이 로드를 설정해야 <code>logger</code>를 통해 sql이 출력됨</p>
<h1 id="jpa-적용-1">JPA 적용 1</h1>
</li>
<li><p><em>Item*</em></p>
</li>
</ul>
<pre><code> @Data
 @Entity
 public class Item {
     @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
     private Long id;
     @Column(name = &quot;item_name&quot;, length = 10)
     private String itemName;
     private Integer price;
     private Integer quantity;

    public Item() {

    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    } 
}
</code></pre><ul>
<li><code>@Entity</code>: JPA가 사용하는 객체라는 뜻이다. </li>
<li><code>@Id</code>: 테이블의 PK와 해당 필드를 매핑</li>
<li><code>@GeneratedValue(strategy=GenerationType.Indetity)</code>: PK 생성값을 데이터 베이스에서 생성하는 <code>identity</code> 방식 사용 (<code>auto_increment</code>)<ul>
<li><code>@Column</code>: 객체의 필드를 테이블의 칼럼과 매핑</li>
<li><code>name</code>: 객체는 <code>itemName</code>이지만 테이블의 컬럼은<code>item_name</code>이므로 이렇게 매핑</li>
<li><code>length=10</code>: <code>varchar(10)</code></li>
<li><code>@Column</code>을 생략하면 필드의 이름을 테이블 컬럼 이름으로 사용</li>
</ul>
</li>
</ul>
<p> <strong>JpaRepositoryV1</strong></p>
<pre><code>@Slf4j
@Repository
@Transactional
public class JpaItemRepository implements ItemRepository {

    private final EntityManager em;

    public JpaItemRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public Item save(Item item) {
        em.persist(item);
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findItem = em.find(Item.class, itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    @Override
    public Optional&lt;Item&gt; findById(Long id) {
        Item item = em.find(Item.class, id);
        return Optional.ofNullable(item);
    }

    @Override
    public List&lt;Item&gt; findAll(ItemSearchCond cond) {
        String jpql = &quot;selectxxx i from Item i&quot;;

        Integer maxPrice = cond.getMaxPrice();
        String itemName = cond.getItemName();

        if (StringUtils.hasText(itemName) || maxPrice != null) {
            jpql += &quot; where&quot;;
        }

        boolean andFlag = false;
        List&lt;Object&gt; param = new ArrayList&lt;&gt;();
        if (StringUtils.hasText(itemName)) {
            jpql += &quot; i.itemName like concat(&#39;%&#39;,:itemName,&#39;%&#39;)&quot;;
            param.add(itemName);
            andFlag = true;
        }

        if (maxPrice != null) {
            if (andFlag) {
                jpql += &quot; and&quot;;
            }
            jpql += &quot; i.price &lt;= :maxPrice&quot;;
            param.add(maxPrice);
        }

        log.info(&quot;jpql={}&quot;, jpql);

        TypedQuery&lt;Item&gt; query = em.createQuery(jpql, Item.class);
        if (StringUtils.hasText(itemName)) {
            query.setParameter(&quot;itemName&quot;, itemName);
        }
        if (maxPrice != null) {
            query.setParameter(&quot;maxPrice&quot;, maxPrice);
        }
        return query.getResultList();
    }
}
</code></pre><ul>
<li><p><code>private final EntityManager em:</code>생성자를 보면 스프링을 통해 엔터티 매니저라는 것을 주입 받은 것을 확인할 수 있다. JPA의 모든 동작은 엔터티 매니저를 통해서 이루어진다. 엔타티 매니저는 내부에 데이터소스를 가지고 있고 데이터베이스에 접근할 수 있다</p>
</li>
<li><p><code>@Transactional</code>: JPA의 모든 데이터 변경은 트랜잭션 안에서 이뤄져야 함</p>
</li>
</ul>
<p> <strong>JpaConfig</strong></p>
<pre><code>Configuration
public class JpaConfig {

    private final EntityManager em;

    public JpaConfig(EntityManager em) {
        this.em = em;
    }

    @Bean
    public ItemService itemService() {
        return new ItemServiceV1(itemRepository());
    }

    @Bean
    public ItemRepository itemRepository() {
        return new JpaItemRepository(em);
    }

}
</code></pre><p> <br><br></p>
<h1 id="jpa-적용-2">JPA 적용 2</h1>
<p> ** public Item save(Item item) **</p>
<pre><code>public Item save(Item item) {
        em.persist(item);
        return item;
    }</code></pre><ul>
<li><p>JPA에서 객체를 테이블에 저장할 때 엔터티 매니저가 제공하는 <code>persist()</code> 메서드를 사용하면 됨</p>
<p><strong>실행된 쿼리</strong></p>
<pre><code>insert into item (id, item_name, price, quantity) values (null, ?, ?, ?)
insert into item (id, item_name, price, quantity) values (default, ?, ?, ?)
insert into item (item_name, price, quantity) values (?, ?, ?)

</code></pre></li>
</ul>
<ul>
<li><code>Item</code> entity를 만들때 PK 값 전략을 <code>IDENTITY</code>로 사용했기 때문에 JPA가 이런 쿼리 생성</li>
<li>쿼리 실행 이후에 <code>Item</code> 객체의 <code>id</code> 필드에 데이터베이스가 생성한 PK값이 들어가게 된다</li>
</ul>
<br>

<p><strong>public void update(Long itemId, ItemUpdateDto updateParam)</strong> </p>
<pre><code>public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findItem = em.find(Item.class, itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }</code></pre><ul>
<li><strong>실행된 쿼리</strong><pre><code>update item set item_name=?, price=?, quantity=? where id=?</code></pre><ul>
<li><code>em.update()</code>같은 값을 전혀 호출하지 않았다. </li>
<li><code>JPA</code>는 트랜잭션이 커밋되는 시점에 변경된 엔티티 객체가 있는지 확인한다. 특정 엔터티 객체가 변경된 경우에는 UPDATE SQL을 실행</li>
</ul>
</li>
</ul>
<br>



<p><strong>public Optional<Item> findById(Long id)</strong></p>
<pre><code> public Optional&lt;Item&gt; findById(Long id) {
        Item item = em.find(Item.class, id);
        return Optional.ofNullable(item);
    }
</code></pre><ul>
<li><strong>실행된 쿼리 값</strong><pre><code>select
item0_.id as id1_0_0_,
item0_.item_name as item_nam2_0_0_,
item0_.price as price3_0_0_,
item0_.quantity as quantity4_0_0_
from item item0_
where item0_.id=?

</code></pre></li>
</ul>
<pre><code>
 &lt;br&gt;&lt;br&gt;

# JPA 적용 3 _ 예외변환

-  `EntityManager` 는 순수한 JPA 기술이고, 스프링과는 관계가 없다. 따라서 엔티티 매니저는 예외가 발생하면 JPA 관련 예외를 발생시킨다.
- JPA는 `PersistenceException` 과 그 하위 예외를 발생시킨다.
- 추가로 JPA는 `IllegalStateException` , `IllegalArgumentException` 을 발생시킬 수 있다. 그렇다면 JPA 예외를 스프링 예외 추상화( `DataAccessException` )로 어떻게 변환할 수 있을까?
- 비밀은 바로 `@Repository` 에 있다.

 ![](https://velog.velcdn.com/images/java_dong/post/e354bb3c-b486-488d-80c9-6da50782b885/image.png)


 &lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;

 출처:https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2









</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 연결 테스트]]></title>
            <link>https://velog.io/@java_dong/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@java_dong/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Tue, 25 Feb 2025 07:42:19 GMT</pubDate>
            <description><![CDATA[<p><strong>데이터베이스 연동_ main_application.properties</strong></p>
<pre><code>properties
 spring.profiles.active=local
 spring.datasource.url=jdbc:h2:tcp://localhost/~/test
 spring.datasource.username=sa
logging.level.org.springframework.jdbc=debug</code></pre><p><strong>데이터베이스 연동_test_application.properties</strong></p>
<pre><code>properties
 spring.profiles.active=test
 spring.datasource.url=jdbc:h2:tcp://localhost/~/test
 spring.datasource.username=sa
logging.level.org.springframework.jdbc=debug</code></pre><p><strong>@SpringbootTest</strong></p>
<ul>
<li><code>@SpringbootTest</code>은 <code>@SpringbootApplication</code>을 찾아서 설정으로 사용</li>
</ul>
<p><strong>테스트와 데이터베이스 분리</strong></p>
<ul>
<li>H2 데이터베이스를 용도에 따라 2가지로 구분<ul>
<li><code>jdbc:h2:tcp://localhost/~/test</code> local에 접근하는 서버 전용 데이터베이스</li>
<li><code>jdbc:h2:tcp://localhost/~/testcase</code> test 케이스에서 사용하는 전용 데이터베이스</li>
</ul>
</li>
</ul>
<p><br><br></p>
<p>** 테스트 데이터 롤백 **</p>
<p>테스트가 끝나고 나서 트랜잭션을 강제로 롤백하면 데이터가 깔끔하게 제거된다. 데이터를 이미 저장했는데 중간에 테스트가 실패해서 롤백을 호출하지 못하더라도 상관 없다. 트랜잭션을 커밋하지 않았기 때문에 데이터베이스에 해당 내용이 반영되지 않음</p>
<p>테스트는 각각의 테스트 실행 전 후로 동작하는 <code>@BeforeEach</code> <code>@AfterEach</code>라는 편리한 기능을 제공</p>
<p><strong>테스트에 직접 트랜잭션 추가</strong></p>
<pre><code>@SpringBootTest
 class ItemRepositoryTest {
     @Autowired
     ItemRepository itemRepository;
      //트랜잭션 관련 코드
      @Autowired
      PlatformTransactionManager transactionManager;
      TransactionStatus status;
      @BeforeEach
      void beforeEach() {
      //트랜잭션 시작
             status = transactionManager.getTransaction(new
               DefaultTransactionDefinition());
      }
      @AfterEach
      void afterEach() {
          //MemoryItemRepository 의 경우 제한적으로 사용
          if (itemRepository instanceof MemoryItemRepository) {
                   ((MemoryItemRepository) itemRepository).clearStore();
               }
      //트랜잭션 롤백
               transactionManager.rollback(status);
           }
      //...
}</code></pre><ul>
<li><code>@BeforeEach</code>: 각각의 테스트 케이스를 실행하기 직전에 호출함 따라서 요기서 트랜잭션을 시작하면 각각의 테스트를 트랜잭션 범위 안에서 실행할 수 있다</li>
<li><code>@AfterEach</code>: 각각의 테스트 케이스가 완료된 직후에 호출 됨 따라서 여기서 롤백하면 데이터를 트랜잭션 실행 전 상태로 복구가 가능 </li>
</ul>
<p><br><br></p>
<p><strong>테스트에서 @Transactional</strong></p>
<ul>
<li>스프링에서 제공하는 <code>@Transactional</code> 에노테이션은 로직이 성공적으로 수행되면 커밋되도록 동작함</li>
<li><code>@Transactional</code>을 테스트에서 사용하면 테스트를 트랜잭션 안에서 실행하고 테스트가 끝나면 트랜잭션을 커밋하는 것이 아닌 자동으로 롤백해버림</li>
<li>롤백으로 테스트 검증이 완료된 후에는 데이터가 삭제</li>
</ul>
<p><strong>강제로 커밋하기_ @Commit</strong>
<code>@Transactional</code>을 테스트에서 사용하면 롤백이 되어버리기 때문에 데이터가 실제로 저장되었는지 확인하는 것이 어렵다. 실제로 데이터가 직접 저장되는지 여부를 확인하려면 <code>@Commit</code>을 클래스 또는 메서드에 붙이면 롤백대신 커밋이 호출된다. 참고로 <code>@Rollback(value=false)</code>를 사용해도 됨</p>
<p><br><br>
<strong>테스트_임베디드 모드</strong></p>
<p>테스트 케이스를 실행하기 위해서 데이터베이스를 별도로 설치하고 운영하는 것은 상당히 번잡스럽다. 단순히 테스트를 검증할 용도로만 사용하므로 테스트가 끝나면 데이터베이스를 아예 지워버려도 된다.</p>
<pre><code>     @Bean
     @Profile(&quot;test&quot;)
     public DataSource dataSource() {
        log.info(&quot;메모리 데이터베이스 초기화&quot;);
          DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(&quot;org.h2.Driver&quot;);
        dataSource.setUrl(&quot;jdbc:h2:mem:db;DB_CLOSE_DELAY=-1&quot;);
        dataSource.setUsername(&quot;sa&quot;);
        dataSource.setPassword(&quot;&quot;);
        return dataSource;
    }</code></pre><ul>
<li>프로필이 <code>test</code>인 경우에만 해당 데이터베이스를 사용하도록 설정</li>
<li>테스트케이스에서만 이 데이터소스를 스프링 빈으로 등록해서 사용하겠다는 뜻</li>
<li><code>jdbc:h2:mem:db</code> 을 쓰면 임베디드 모드로 동작하는 데이터베이스를 만들 수 있다</li>
</ul>
<p><strong>주의</strong></p>
<ul>
<li>메모리 DB는 애플리케이션이 종료될 때 함께 사라지기 때문에 애플리케이션 실행 시점에 데이터베이스 테이블도 새로 만들어 줘야 함</li>
<li><code>src/test/resources/schema.sql</code>에 원하는 데이터베이스를 만들면 이 문제가 해결<pre><code>drop table if exists item CASCADE;
create table item
(
   id        bigint generated by default as identity,
   item_name varchar(10),
   price     integer,
   quantity  integer,
   primary key (id)
);</code></pre></li>
</ul>
<p>출처:<a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터 접근기술]]></title>
            <link>https://velog.io/@java_dong/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%91%EA%B7%BC%EA%B8%B0%EC%88%A0</link>
            <guid>https://velog.io/@java_dong/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%91%EA%B7%BC%EA%B8%B0%EC%88%A0</guid>
            <pubDate>Sun, 23 Feb 2025 07:08:47 GMT</pubDate>
            <description><![CDATA[<h1 id="데이터-접근-기술-종류">데이터 접근 기술 종류</h1>
<p><strong>적용 데이터 접근 기술</strong></p>
<ul>
<li>Jdbc Template</li>
<li>MyBatis</li>
<li>JPa, Hibernate</li>
<li>스프링 데이터 Jpa</li>
<li>Querydsl</li>
</ul>
<p><strong>SQL Mapper</strong></p>
<ul>
<li>Jdbc Template</li>
<li>MyBatis</li>
<li><em>ORM 관련 기술*</em></li>
<li>Jpa, Hibernate</li>
<li>스프링 데이터 JPA</li>
<li>QueryDsl</li>
</ul>
<p><strong>SqL Mapper 주요 기능</strong></p>
<ul>
<li>개발자는 SQL만 작성하면 해당 SQl의 결과를 객체로 편리하게 매칭해 준다</li>
<li>JDBC를 직접 사용할 때 발생하는 여러가지 중복을 제거해 주고 기타 개발자에게 여러가지 편리한 기능을 제공해준다</li>
</ul>
<p><strong>ORM 주요기능</strong></p>
<ul>
<li>Jdbc Template이나 MyBatis 같은 SQL 을 개발자가 직접 작성해야 하지만 JPA를 사용하면 기본적인 SQL은 JPA가 대신 작성</li>
</ul>
<p><br><br></p>
<h1 id="도메인-분석">도메인 분석</h1>
<p><strong>Item</strong></p>
<pre><code> @Data
 public class Item {
     private Long id;
     private String itemName;
     private Integer price;
     private Integer quantity;
     public Item() {
     }
     public Item(String itemName, Integer price, Integer quantity) {
         this.itemName = itemName;
         this.price = price;
         this.quantity = quantity;
    } 
}</code></pre><p><strong>ItemRepository 인터페이스</strong></p>
<pre><code>public interface ItemRepository {
     Item save(Item item);
     void update(Long itemId, ItemUpdateDto updateParam);
     Optional&lt;Item&gt; findById(Long id);
     List&lt;Item&gt; findAll(ItemSearchCond cond);
}</code></pre><p><strong>ItemSearchCond</strong></p>
<pre><code>@Data
 public class ItemSearchCond {
     private String itemName;
     private Integer maxPrice;
     public ItemSearchCond() {
     }
     public ItemSearchCond(String itemName, Integer maxPrice) {
         this.itemName = itemName;
         this.maxPrice = maxPrice;
    }
}</code></pre><ul>
<li>검색조건으로 활용됨, 상품명, 최대 가격이 있다. 참고로 상품명의 일부만 포함되어있더라도 검색이 가능해야 함</li>
</ul>
<p><strong>ItemUpdateDto</strong></p>
<pre><code>
 @Data
 public class ItemUpdateDto {
     private String itemName;
     private Integer price;
     private Integer quantity;
     public ItemUpdateDto() {
     }
     public ItemUpdateDto(String itemName, Integer price, Integer quantity) {
         this.itemName = itemName;
         this.price = price;
         this.quantity = quantity;
     }
 }</code></pre><ul>
<li>상품을 수정할 때 사용하는 객체</li>
<li>단순히 데이터를 전달하는 용도로만 사용됨</li>
</ul>
<p><strong>DTO 전송객체</strong></p>
<ul>
<li>데이터 전송객체</li>
<li>DTO는 기능은 없고 단순히 데이터를 전달만 하는 용도로 사용되는 객체</li>
</ul>
<p><strong>ItemRepository 구현체</strong></p>
<pre><code>@Repository
 public class MemoryItemRepository implements ItemRepository {
     private static final Map&lt;Long, Item&gt; store = new HashMap&lt;&gt;(); //static
     private static long sequence = 0L; //static
     @Override
     public Item save(Item item) {
         item.setId(++sequence);
         store.put(item.getId(), item);
         return item;
         }
      @Override
      public void update(Long itemId, ItemUpdateDto updateParam) {
          Item findItem = findById(itemId).orElseThrow();
          findItem.setItemName(updateParam.getItemName());
          findItem.setPrice(updateParam.getPrice());
          findItem.setQuantity(updateParam.getQuantity());
      }
      @Override
      public Optional&lt;Item&gt; findById(Long id) {
          return Optional.ofNullable(store.get(id));
      }
      @Override
      public List&lt;Item&gt; findAll(ItemSearchCond cond) {
          String itemName = cond.getItemName();
          Integer maxPrice = cond.getMaxPrice();
          return store.values().stream().filter(item-&gt;{
          if(ObjectUtils.isEmpty(itemName)){
                  return true;
              }
            return item.getPrice()&lt;=maxPrice;

          })
          .collect(Collectors.toList());
      }

    public void clearStore() {
        store.clear();
    }
}</code></pre><ul>
<li><p><code>ItemRepository</code> 인터페이스를 구현한 메모리 저장소이다</p>
</li>
<li><p>메모리이기 때문에 자바를 다시 실행하면 기존에 저장된 모든 데이터가 사라짐</p>
</li>
<li><p><code>save</code>,<code>update</code>,<code>findById</code>는 쉽게 이해할 수 있다</p>
</li>
<li><p>참고로 <code>optional</code>값을 반환하면 값이 존재하지 않으면 <code>ofNullable</code>로 오류가 안나게 할 수 있다</p>
</li>
<li><p><code>findAll</code>은 <code>ItemSearchCond</code>이라는 검색 조건을 받아서 내부에서 데이터를 검색하는 기능 수행</p>
<ul>
<li><code>itemName</code>이나 <code>maxPrice</code>가 빈값이면 해당 조건 무시</li>
<li>값이 있을 때만 해당 조건으로 필터링 기능 수행</li>
</ul>
</li>
</ul>
<p><br><br></p>
<h1 id="서비스-분석">서비스 분석</h1>
<p><strong>ItemService 인터페이스</strong></p>
<pre><code>public interface ItemService {
     Item save(Item item);
     void update(Long itemId, ItemUpdateDto updateParam);
     Optional&lt;Item&gt; findById(Long id);
     List&lt;Item&gt; findItems(ItemSearchCond itemSearch);
 }</code></pre><p><strong>ItemServiceV1</strong></p>
<pre><code>@Service
@RequiredArgsConstructor
public class ItemServiceV1 implements ItemService {
    private final ItemRepository itemRepository;
    @Override
    public Item save(Item item) {
        return itemRepository.save(item);
    }
    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        itemRepository.update(itemId, updateParam);
    }
    @Override
    public Optional&lt;Item&gt; findById(Long id) {
        return itemRepository.findById(id);
    }
    @Override
    public List&lt;Item&gt; findItems(ItemSearchCond cond) {
        return itemRepository.findAll(cond);
    }
}</code></pre><ul>
<li>이 구현체는 대부분의 기능을 단순히 저장소에 위임한다</li>
</ul>
<p><br><br></p>
<h1 id="컨트롤러-분석">컨트롤러 분석</h1>
<p><strong>HomeController</strong></p>
<pre><code> @Controller
 @RequiredArgsConstructor
 public class HomeController {
     @RequestMapping(&quot;/&quot;)
     public String home() {
         return &quot;redirect:/items&quot;;
     }
}
</code></pre><ul>
<li>단순히 홈으로 요청이 왔을 때 <code>items</code>로 이동하는 컨트롤러</li>
<li><em>ItemController*</em><pre><code>@Controller
@RequestMapping(&quot;/items&quot;)
@RequiredArgsConstructor
public class ItemController {
    private final ItemService itemService;
@GetMapping
    public String items(@ModelAttribute(&quot;itemSearch&quot;) ItemSearchCond itemSearch,
Model model) {
        List&lt;Item&gt; items = itemService.findItems(itemSearch);
       model.addAttribute(&quot;items&quot;, items);
       return &quot;items&quot;;
   }
   @GetMapping(&quot;/{itemId}&quot;)
   public String item(@PathVariable long itemId, Model model) {
       Item item = itemService.findById(itemId).get();
       model.addAttribute(&quot;item&quot;, item);
       return &quot;item&quot;;
   }
   @GetMapping(&quot;/add&quot;)
   public String addForm() {
       return &quot;addForm&quot;;
   }
   @PostMapping(&quot;/add&quot;)
   public String addItem(@ModelAttribute Item item, RedirectAttributes
redirectAttributes) {
       Item savedItem = itemService.save(item);
       redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
       redirectAttributes.addAttribute(&quot;status&quot;, true);
       return &quot;redirect:/items/{itemId}&quot;;
   }
   @GetMapping(&quot;/{itemId}/edit&quot;)
   public String editForm(@PathVariable Long itemId, Model model) {
       Item item = itemService.findById(itemId).get();
       model.addAttribute(&quot;item&quot;, item);
       return &quot;editForm&quot;;
}
   @PostMapping(&quot;/{itemId}/edit&quot;)
   public String edit(@PathVariable Long itemId, @ModelAttribute ItemUpdateDto
updateParam) {
       itemService.update(itemId, updateParam);
       return &quot;redirect:/items/{itemId}&quot;;
   }
}</code></pre><br><br></li>
</ul>
<h1 id="스프링부트-설정-분석">스프링부트 설정 분석</h1>
<p><strong>MemoryConfig</strong></p>
<pre><code>@Configuration
 public class MemoryConfig {
     @Bean
     public ItemService itemService() {
         return new ItemServiceV1(itemRepository());
     }
     @Bean
     public ItemRepository itemRepository() {
         return new MemoryItemRepository();
     }
}</code></pre><ul>
<li><code>itemServiceV1</code>,<code>MemoryItemRepository</code>를 스프링 빈으로 등록하고 생성자를 통해 의존관계를 주입</li>
<li>구현체를 편리하게 관리하기 위해 수동으로 빈 등록</li>
<li>컨트롤러는 컴포넌트 스캔을 사용</li>
</ul>
<p><strong>TestDataInit</strong></p>
<pre><code>@Slf4j
@RequiredArgsConstructor
public class TestDataInit {
    private final ItemRepository itemRepository;
/**
* 확인용 초기 데이터 추가 */
    @EventListener(ApplicationReadyEvent.class)
    public void initData() {
        log.info(&quot;test data init&quot;);
        itemRepository.save(new Item(&quot;itemA&quot;, 10000, 10));
        itemRepository.save(new Item(&quot;itemB&quot;, 20000, 20));
    } 
}</code></pre><ul>
<li>애플리케이션을 실행할 때 초기 데이터를 저장</li>
<li>리스트에서 데이터가 잘 나오는지 편리하게 확인할 용도로 사용</li>
<li><code>@EventListener(ApplicationReadyEvent.class)</code> : 스프링 컨테이너가 완전히 초기화를 띁내놓고 실행 준비가 되었을 때 발생하는 이벤트이다. 스프링이 이 시점에 해당 애너테이션이 붙은 메서드를 호출함</li>
</ul>
<p><strong>ItemServiceApplication</strong></p>
<pre><code>@Import(MemoryConfig.class)
@SpringBootApplication(scanBasePackages = &quot;hello.itemservice.web&quot;)
public class ItemServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ItemServiceApplication.class, args);
    }
    @Bean
    @Profile(&quot;local&quot;)
    public TestDataInit testDataInit(ItemRepository itemRepository) {
        return new TestDataInit(itemRepository);
    }
}
</code></pre><ul>
<li><code>@Import(MemoryConfig.class)</code> : 앞서 설정한 <code>MemoryConfig</code>를 설정 파일로 사용</li>
<li><code>scanBasePackages = &quot;hello.itemservice.web&quot;</code> : 여기서는 컨트롤러만 컴포넌트 스캔을 사용하고 나머지는 직접 수동 등록함. 그래서 컴포넌트 스캔 경로를 <code>hello.itemservice.web</code>하위로 설정함</li>
<li><code>@Profile(&quot;local&quot;)</code> : 특정 프로필의 경우에만 해당 스프링 빈을 등록. 여기서는 <code>local</code>이라는 이름의 프로필이 사용되는 경우에만 <code>testDataInit</code>이라는 스프링 빈을 등록</li>
</ul>
<p><strong>프로필</strong>
스프링은 로딩 시점에 <code>application.properties</code>의 <code>spring.profiles.active</code> 속성을 읽어서 프로필로 사용함</p>
<p>이 프로필은 다양한 환경에 따라서 다른 설정을 할때 사용하는 정보이다. 예를 들어서 로컬 PC에서는 로컬 PC에 저장된 데이터베이스에 접근해야하고 운영 환경에서는 운영 데이터베이스에 접근해야한다면 서로 설정 정보가 달라야한다. 심지어 환경에 따라서 다른 스프링 빈을 등록할 수 있어야한다. 프로필은 이런 문제를 해결하는데 도움을 준다</p>
<p><code>/src/main/resources</code> 하위의 <code>application.properties</code></p>
<pre><code>spring.profiles.active=local</code></pre><ul>
<li>이 위치의 <code>application.properties</code> 는 <code>/src/main</code> 하위의 자바 객체를 실행할 때 (주로 <code>main()</code> ) 동작 하는 스프링 설정이다. <code>spring.profiles.active=local</code> 이라고 하면 스프링은 <code>local</code> 이라는 프로필로 동작한다. 따라서 직전에 설명한 <code>@Profile(&quot;local&quot;)</code> 가 동작하고, <code>testDataInit</code> 가 스프링 빈으로 등록 된다.</li>
</ul>
<p><strong>테스트_ ItemRepositoryTest</strong></p>
<pre><code>@SpringBootTest
 class ItemRepositoryTest {
     @Autowired
     ItemRepository itemRepository;

     @AfterEach
     void afterEach() {
//MemoryItemRepository 의 경우 제한적으로 사용
        if (itemRepository instanceof MemoryItemRepository) {
          ((MemoryItemRepository) itemRepository).clearStore();
        }
    }
    @Test
    void save() {
    //given
        Item item = new Item(&quot;itemA&quot;, 10000, 10);
        //when
        Item savedItem = itemRepository.save(item);
    //then
        Item findItem = itemRepository.findById(item.getId()).get();
        assertThat(findItem).isEqualTo(savedItem);
    }
    @Test
    void updateItem() {
    //given
        Item item = new Item(&quot;item1&quot;, 10000, 10);
        Item savedItem = itemRepository.save(item);
        Long itemId = savedItem.getId();
    //when
        ItemUpdateDto updateParam = new ItemUpdateDto(&quot;item2&quot;, 20000, 30);
        itemRepository.update(itemId, updateParam);
    //then
        Item findItem = itemRepository.findById(itemId).get();
        assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
        assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
        assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
    }
    @Test
    void findItems() {
    //given
        Item item1 = new Item(&quot;itemA-1&quot;, 10000, 10);
        Item item2 = new Item(&quot;itemA-2&quot;, 20000, 20);
        Item item3 = new Item(&quot;itemB-1&quot;, 30000, 30);
        itemRepository.save(item1);
         itemRepository.save(item2);
        itemRepository.save(item3);
        //둘 다 없음 검증
        test(null, null, item1, item2, item3); 
        test(&quot;&quot;, null, item1, item2, item3);
        //itemName 검증
        test(&quot;itemA&quot;, null, item1, item2);
        test(&quot;temA&quot;, null, item1, item2);
        test(&quot;itemB&quot;, null, item3);
        //maxPrice 검증
        test(null, 10000, item1);
        //둘 다 있음 검증
        test(&quot;itemA&quot;, 10000, item1);
    }
    void test(String itemName, Integer maxPrice, Item... items) {
        List&lt;Item&gt; result = itemRepository.findAll(new ItemSearchCond(itemName,maxPrice));
        assertThat(result).containsExactly(items);
    } 
}</code></pre><p><code>afterEach()</code> : 테스트는 서로 영향을 주면 안된다. 따라서 각각의 테스트가 끝나고 나면 저장한 데이터를 제거 해야 한다. <code>@AfterEach</code> 는 각각의 테스트의 실행이 끝나는 시점에 호출된다. 여기서는 메모리 저장소를 완전히 삭제해서 다음 테스트에 영향을 주지 않도록 초기화 한다.</p>
<p><br><br><br><br>
출처:<a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Node 객체]]></title>
            <link>https://velog.io/@java_dong/Node-%EA%B0%9D%EC%B2%B4</link>
            <guid>https://velog.io/@java_dong/Node-%EA%B0%9D%EC%B2%B4</guid>
            <pubDate>Sun, 23 Feb 2025 01:09:40 GMT</pubDate>
            <description><![CDATA[<p><strong>1</strong></p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;

&lt;body id=&quot;start&quot;&gt; 
    &lt;ul&gt;
        &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;html&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;css&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;자바스크립트&lt;/a&gt;
            &lt;ul&gt; 
                &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;자바스크립트 core&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;DOM&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;BOM&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/li&gt;
     &lt;/ul&gt; 
    &lt;script&gt;
        // Node 관계 API
        var start = document.getElementById(&#39;start&#39;);
        console.log( start.firstChild);
        console.log(start.firstChild.nextSibling);
        start.firstChild.nextSibling.firstChild.nextSibling.style.color=&#39;red&#39;; 
        // 이런식으로 스타일을 조정할 수도있다
        console.log(start.firstChild.nextSibling.nextSibling);
        console.log(start.firstChild.nextSibling.nextSibling.nextSibling);

        start.childNodes;
        // if문을 건 이유는 줄바꿈 노드가 존재하여 그냥 돌리면 에러가 발생하기 때문
        for(var i=0;i&lt;start.childNodes.length;++i){
            if(typeof start.childNodes[i] !== &#39;text&#39;){
                start.childNodes[i].color=&#39;blue&#39;;
            }
        }
    &lt;/script&gt;


&lt;/body&gt;
&lt;/html&gt;

&lt;!-- body 밑에는 줄바꿈이라는 것이 있어 firstChild를 걸어도 text값이 나옴
    반면 줄바꿈을 없애면 firstChild 값에 ul이 나옴 --&gt;
    v    &lt;!-- Node.childNodes &lt;/br&gt;
    Node.firstChild&lt;/br&gt;
    Node.lastChild&lt;/br&gt;
    Node.nextSibling&lt;/br&gt;
    Node.previousSibling&lt;/br&gt;
    Node.contains()&lt;/br&gt;
    Node.hasChildNodes()&lt;/br&gt;
    &lt;p&gt;&lt; node값&gt;&lt;/node&gt;&lt;/p&gt;
    Node.nodeType&lt;/br&gt;
    Node.nodeName&lt;/br&gt;

    Node.nodeValue&lt;/br&gt;
    Node.textContent&lt;/br&gt;

    Node.appendChild()&lt;/br&gt;
    Node.removeChild()&lt;/br&gt; --&gt;</code></pre><p><strong>2</strong></p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body id=&quot;start&quot;&gt;
    &lt;ul&gt;
        &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;html&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;css&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;자바스크립트&lt;/a&gt;
            &lt;ul&gt; 
                &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;자바스크립트 core&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;DOM&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;./1234&quot;&gt;BOM&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/li&gt;
     &lt;/ul&gt; 
    &lt;script&gt;
        for (var name in Node){
            // 상수가 어떻게 활용되는가?
            console.log(name,Node[name]);
        }
        var body = document.getElementById(&#39;start&#39;);
        console.log(body.nodeType);

        console.log(body.firstChild.nodeType);
        // 3은 text_node이다
        // 노드타입을 활용할때 사용한다
        console.log(body.firstChild.nodeType===Node.TEXT_NODE);
        // true값이 나옴

        // nodeName은 태그의 이름 또는 #text로 정보를 구체적으로 전달한다
        console.log(body.firstChild.nextSibling.nodeName);
        // nodeType이 1이면 elementType
        // 3이면 text로 이를 넣으면 오류가 발생한다

        function transverse(target,callback){
            if(target.nodeType===1){
                callback(target);
                var c = target.childNodes;
                for(var i=0;i&lt;c.length;++i){
                    transverse(c[i],callback);
                }
            }
        }
        transverse(document.getElementById(&#39;start&#39;),function(ele){
            console.log(ele);
            ele.style.color=&#39;red&#39;;
            if(ele.nodeName===&#39;A&#39;){
                ele.style.backgroundColor=&#39;blue&#39;;
            }
        })
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><p><strong>3</strong></p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body id=&quot;start&quot;&gt;
   &lt;ul id=&quot;target&quot;&gt;
        &lt;li&gt;HTML&lt;/li&gt;
        &lt;li&gt;CSS&lt;/li&gt;
    &lt;/ul&gt;
    &lt;input type=&quot;button&quot; value=&quot;appendChild2()&quot; onclick=appendChild2();&gt;
    &lt;input type=&quot;button&quot; value=&quot;insertBefore()&quot; onclick=insertBefore2();&gt; &lt;!-- 따옴표 추가 --&gt;
    &lt;ul&gt;
        &lt;li&gt;html&lt;/li&gt;
        &lt;li&gt;css&lt;/li&gt;
        &lt;li id=&quot;target1&quot;&gt;java&lt;/li&gt;
    &lt;/ul&gt;
    &lt;input type=&quot;button&quot; value=&quot;callremove()&quot; onclick=&quot;callremove();&quot;&gt;
    &lt;input type=&quot;button&quot; value=&quot;callreplace()&quot; onclick=callreplace();&gt;
    &lt;script&gt;

        var li = document.createElement(&#39;li&#39;);
            var text = document.createTextNode(&#39;JavaScript&#39;); // 대소문자 통일
            li.appendChild(text);
            target.appendChild(li);
        // appendChild(): 새로운 노드를 마지막에 추가
        function appendChild2(){
            console.log(document.getElementById
        (&#39;target&#39;));
            var target = document.getElementById
            (&#39;target&#39;);
            con
            var li = document.createElement(&#39;li&#39;);
            var text = document.createTextNode(&#39;JavaScript&#39;); // 대소문자 통일
            li.appendChild(text);
            target.appendChild(li);
        }

        // insertBefore(): 리스트의 첫 번째 요소 앞에 삽입
        function insertBefore2(){
            var target = document.getElementById(&#39;target&#39;);
            var li = document.createElement(&#39;li&#39;);

            var text = document.createTextNode(&#39;jQuery&#39;); // 대소문자 통일

            var aTag = document.createElement(&#39;a&#39;);
            // 네이버페이지로 이동하는 노드
            aTag.setAttribute(&#39;href&#39;,&quot;https://www.naver.com&quot;);
            var aText = document.createTextNode(&quot;naver page&quot;);
            aTag.appendChild(aText);
            li.appendChild(text);
            console.log(aTag);
            var firstChild = target.firstElementChild; // 첫 번째 &lt;li&gt; 요소 찾기
            target.insertBefore(li, firstChild);
            target.insertBefore(aTag,firstChild);
        }

        function callremove(){
            var target= document.getElementById(&quot;target1&quot;);
            // 삭제하려는 부모를 알아야지 삭제가 가능하다
            target.parentNode.removeChild(target);
        }
        function callreplace(){
            var a=document.createElement(&#39;a&#39;);
            a.setAttribute(&#39;href&#39;,&#39;https://www.naver.com&#39;);
            a.appendChild(document.createTextNode(&quot;naver.com&quot;));
            console.log(a);
            var target=document.getElementById(&#39;target1&#39;);
            target.replaceChild(a,target.firstChild);
        }
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre><p><strong>4</strong></p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;script src=&quot;../jquery.js&quot;&gt;&lt;/script&gt;
    &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;!--
    - before/ after 해당태그 밖에
    - prepend/append 해당태크안에
    --&gt;
    &lt;div class=&quot;target&quot;&gt;
        content1
    &lt;/div&gt;
    &lt;div class=&quot;target&quot;&gt;
        content2
    &lt;/div&gt;

    &lt;div class=&quot;target1&quot; id=&quot;target1&quot;&gt;
        content3
    &lt;/div&gt;
    &lt;div class=&quot;target1&quot; id=&quot;target2&quot;&gt;
        content4
    &lt;/div&gt;

    &lt;div class=&quot;target1&quot; id=&quot;target3&quot;&gt;
        content3
    &lt;/div&gt;
    &lt;div class=&quot;target1&quot; id=&quot;target4&quot;&gt;
        content4
    &lt;/div&gt;
    &lt;div id=&quot;source&quot;&gt;
        source
    &lt;/div&gt;
    &lt;input type=&quot;button&quot; value=&quot;remove&quot; id=&quot;btn1&quot; &gt;
    &lt;input type=&quot;button&quot; value=&quot;empty&quot; id=&quot;btn2&quot;&gt;
    &lt;input type=&quot;button&quot; value=&quot;replaceAll&quot; id=&quot;btn3&quot; &gt;
    &lt;input type=&quot;button&quot; value=&quot;replaceWith&quot; id=&quot;btn4&quot;&gt;
    &lt;input type=&quot;button&quot; value=&quot;clone replaceAll&quot; id=&quot;btn5&quot; &gt;
    &lt;input type=&quot;button&quot; value=&quot;clone replaceWith&quot; id=&quot;btn6&quot;&gt;
    &lt;script&gt;
        // jquery에서 노드를 변경하는 api
        $(&#39;.target&#39;).before(&#39;&lt;div&gt;before&lt;/div&gt;&#39;);
        $(&#39;.target&#39;).after(&#39;&lt;div&gt;after&lt;/div&gt;&#39;);
        $(&#39;.target&#39;).prepend(&#39;&lt;div&gt;prepend&lt;/div&gt;&#39;);
        $(&#39;.target&#39;).append(&#39;&lt;div&gt;append&lt;/div&gt;&#39;);

        // 제거
        $(&#39;#btn1&#39;).click(function(){
            $(&#39;#target1&#39;).remove();
        })

        $(&#39;#btn2&#39;).click(function(){
            $(&#39;#target2&#39;).empty();
        })

        // 바꾸기 
        $(&#39;#btn3&#39;).click(function(){
            $(&#39;&lt;div&gt;replaceAll&lt;/div&gt;&#39;).replaceAll(&#39;#target3&#39;);
        })
        $(&#39;#btn4&#39;).click(function(){
            $(&#39;#target4&#39;).replaceWith(&#39;&lt;div&gt;replaceWith&lt;/div&gt;&#39;);
        })

        // 노드의 복사 clone ==&gt; 값을 복사해서 변경
        $(&#39;#btn5&#39;).click(function(){
            $(&#39;#source&#39;).clone().replaceAll(&#39;#target1&#39;);
        })
        $(&#39;#btn6&#39;).click(function(){
            $(&#39;#target2&#39;).replaceWith($(&#39;#source&#39;).clone());
        })
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><p><strong>5</strong></p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Document&lt;/title&gt;
    &lt;script src=&quot;../jquery.js&quot;&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;ul id=&quot;target&quot;&gt;
        &lt;li&gt;HTML&lt;/li&gt;
        &lt;li&gt;CSS&lt;/li&gt;
    &lt;/ul&gt;
    &lt;input type=&quot;button&quot; value=&quot;get&quot; onclick=&quot;get();&quot;&gt;
    &lt;input type=&quot;button&quot; value=&quot;set&quot; onclick=&quot;set();&quot;&gt;
    &lt;input type=&quot;button&quot; value=&quot;before begin&quot; onclick=&quot;beforebegin();&quot;&gt;
    &lt;input type=&quot;button&quot; value=&quot;after begin&quot; onclick=&quot;afterbegin();&quot;&gt;
    &lt;input type=&quot;button&quot; value=&quot;before end&quot; onclick=&quot;beforeend();&quot;&gt;
    &lt;input type=&quot;button&quot; value=&quot;after end&quot; onclick=&quot;afterend();&quot;&gt;
    &lt;script&gt;
        // 문자열로 노드제어
         function get(){
            var target = document.getElementById(&#39;target&#39;);
            alert(target.outerHTML); // 자기자신을 포함한 전부
            alert(target.innerText);
         }

         function set(){
            var target = document.getElementById(&#39;target&#39;);
            target.innerHTML += &quot;&lt;li&gt; javascript core&lt;/li&gt;&lt;li&gt;BOM&lt;/li&gt;&lt;li&gt;DOM&lt;/li&gt;&quot;;

            target.innerText=&quot;&lt;li&gt; javascript core&lt;/li&gt;&lt;li&gt;BOM&lt;/li&gt;&lt;li&gt;DOM&lt;/li&gt;&quot;;
         }

        function beforebegin(){
            var target = document.getElementById(&#39;target&#39;);
            target.insertAdjacentHTML(&#39;beforebegin&#39;,&#39;&lt;h1&gt;client side&lt;/h1&gt;&#39;);
        }

        function afterbegin(){
            var target = document.getElementById(&#39;target&#39;);
            target.insertAdjacentHTML(&#39;afterbegin&#39;,&#39;&lt;li&gt;HTML&lt;/li&gt;&#39;);

        }
        function beforeend(){
            var target = document.getElementById(&#39;target&#39;);
            target.insertAdjacentHTML(&#39;beforebegin&#39;,&#39;&lt;li&gt;java script&lt;/li&gt;&#39;);
        }

        function afterend(){
            var target = document.getElementById(&#39;target&#39;);
            target.insertAdjacentHTML(&#39;afterend&#39;,&#39;&lt;h1&gt;server side&lt;/h1&gt;&#39;);
        }
        //  $(&#39;#target&#39;).map(function(index,ele){
        //     var t = document.getElementById(&#39;target&#39;);
        //     var a = document.createElement(&#39;a&#39;);
        //     a.setAttribute(&#39;href&#39;,&#39;https://www.naver.com&#39;);
        //     a.appendChild(document.createTextNode(&#39;naver이동&#39;));
        //     t.appendChild(a);
        //  })
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Element 객체]]></title>
            <link>https://velog.io/@java_dong/Element-%EA%B0%9D%EC%B2%B4</link>
            <guid>https://velog.io/@java_dong/Element-%EA%B0%9D%EC%B2%B4</guid>
            <pubDate>Sun, 23 Feb 2025 00:37:41 GMT</pubDate>
            <description><![CDATA[<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Document&lt;/title&gt;
    &lt;script src=&quot;../jquery.js&quot;&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;a id=&quot;target&quot; href=&quot;http://opentutorials.org&quot;&gt;opentutorials&lt;/a&gt;
    &lt;a id=&quot;t1&quot; href=&quot;http://opentutorials.org&quot;&gt;opentutorials&lt;/a&gt;
    &lt;input type=&quot;checkbox&quot; id=&quot;t2&quot; checked=&quot;checked&quot;&gt;
    &lt;ul&gt;
        &lt;li class=&quot;marked&quot;&gt;html&lt;/li&gt;
        &lt;li&gt;class&lt;/li&gt;
        &lt;li id=&quot;active&quot;&gt;java
            &lt;ul&gt;
                &lt;li&gt;core&lt;/li&gt;
                &lt;li class=&quot;marked&quot;&gt;DOM&lt;/li&gt;
                &lt;li class=&quot;marked&quot;&gt;BOM&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
    &lt;script&gt;
        var t = $(&#39;#target&#39;);
        console.log(t.attr(&#39;href&#39;));
        t.attr(&#39;title&#39;,&#39;opentutorials&#39;)
        t.removeAttr(&#39;title&#39;);

        var t1= $(&#39;#t1&#39;);
        console.log(t1.attr(&#39;href&#39;));
        console.log(t1.prop(&#39;href&#39;));
        var t2=$(&#39;#t2&#39;);
        console.log(t2.attr(&#39;checked&#39;));
        console.log(t2.prop(&#39;checked&#39;));
        t2.removeAttr(&#39;checked&#39;);

        // 제이쿼리에서 특정 하위 부분의 엘리먼트만 찾고자할떄는?
        $(&#39;.marked&#39;,&#39;#active&#39;).css(&#39;background-color&#39;,&#39;red&#39;);
        // $(&#39;#active&#39;,&#39;.marked&#39;).css(&#39;background-color&#39;,&#39;blue&#39;);
        // 이렇게하면 작동되지 x 오른쪽에 있는 게 상위 클래스인거 같다
        $(&quot;#active .marked&quot;).css(&#39;background-color&#39;,&#39;green&#39;);
        // 띄워 쓰기를 해야지 작동한다
        $(&#39;#active&#39;).find(&#39;.marked&#39;).css(&#39;background-color&#39;,&#39;blue&#39;)
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[자바스크립트_DOM]]></title>
            <link>https://velog.io/@java_dong/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8DOM</link>
            <guid>https://velog.io/@java_dong/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8DOM</guid>
            <pubDate>Fri, 21 Feb 2025 05:21:21 GMT</pubDate>
            <description><![CDATA[<p><strong>1</strong></p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Document&lt;/title&gt;
    &lt;style&gt;
        li {padding: 10px; list-style: none;}
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;input type=&quot;button&quot; value=&quot;alert&quot; onclick=&quot;alertFunc();&quot;&gt;
    &lt;input type=&quot;button&quot; value=&quot;confirm&quot; onclick=&quot;func_confirm();&quot;&gt;
    &lt;input type=&quot;button&quot; value=&quot;prompt&quot; onclick=&quot;func_prompt();&quot;&gt;
    &lt;/br&gt;
    &lt;input type=&quot;button&quot; value=&quot;window.open(demo2)&quot; onclick=&quot;open1();&quot;&gt;    &lt;/br&gt;

    두번째 인자는 새창의 이름이다. _self는 스크립트가 실행되는창을의미    &lt;/br&gt;
    &lt;input type=&quot;button&quot; value=&quot;window.open(demo2,_self)&quot; onclick=&quot;open2();&quot;&gt;    &lt;/br&gt;

    두번째 인자는 새창의 이름이다. _blank는 새창을의미    &lt;/br&gt;
    &lt;input type=&quot;button&quot; value=&quot;window.open(demo2,_blank)&quot; onclick=&quot;open3();&quot;&gt;&lt;/br&gt;

    창에 이름을 붙일 수 있다 open을 재실행했을때 동일한 이름의 창이있으면 그곳으로 문서가 로드   &lt;/br&gt;
    &lt;input type=&quot;button&quot; value=&quot;window.open(demo2,&#39;ot&#39;)&quot; onclick=&quot;open4();&quot;&gt;&lt;/br&gt;

    창크기 조절하는 세번쨰 인자  &lt;/br&gt;
    &lt;input type=&quot;button&quot; value=&quot;window.open(demo2,&#39;ot&#39;,---)&quot; onclick=&quot;open5();&quot;&gt;&lt;/br&gt;
    &lt;input type=&quot;button&quot; value=&quot;open&quot; onclick=&quot;winopen()&quot;&gt;
    &lt;input type=&quot;text&quot; onkeypress=&quot;winmessage(this.value)&quot;&gt;
    &lt;input type=&quot;button&quot; value=&quot;close&quot; onclick=&quot;winclose()&quot;&gt;
    &lt;script&gt;
        var win=window;
        function winopen(){
            win=win.open(&#39;demo2.html&#39;,&#39;ot&#39;);
            console.dir(win.pathname);
            console.dir(win);
        }
        function winmessage(msg){
            win.document.getElementById(&quot;message&quot;).innerText=msg;
        }
        function winclose(){
            win.close();
        }
        function alertFunc(){
            alert(1);
        }

        function func_confirm(){
            if(confirm(&#39;데이터를 변경할래?&#39;)){
                alert(&#39;ok&#39;)
            }else{
                alert(&#39;cancel&#39;);
            }
        }

        function func_prompt(){
            if(prompt(&#39;id를 입력&#39;)===&#39;lee&#39;){
                alert(&#39;welcomd&#39;);
            }else{
                alert(&#39;fail&#39;);
            }
        }
        //  BOM이란 웹 브라우저의 창이나 프레임을 추상화해서 프로그래밍적으로 제어할 수 있도록 제공하는 수단 Window 객체의 사용법을 아는 것이라 할 수 있다

        // 전역객체 window
        // alert(&#39;Hello world!&#39;)
        // window.alert(&quot;hello wordl!&quot;) // 위의 내장함수는 앞에 window가 생략된것

        var a=1;
        a;
        window.a;


        // Location객체 =&gt; 현재 윈도우의 URL 알려준다
        console.log(location.toString,location.href)
        console.log(location);

        // alert(location); 이런식으로도 가능하다

        console.log(location.protocol);    
        console.log(location.host);
        console.log(location.port);// 컴퓨터에서 서버 소프트웨어를 식별하는 것
        console.log(location.pathname);    
        console.log(location.hash);

        // location은 url주소를 변경하거나 리로드를 할수 있다
        // location.href=&#39;/study/array.html&#39;; 

        // location.href =location.href; // 리로드
        // location.reload(); 리로드를 할수있다

        // Navigator 객체
        console.dir(navigator);
        console.dir(navigator.appName);
        console.dir(navigator.appVersion);

        console.dir(navigator.platform);

        // window 창제어
        function open1(){
            window.open(&#39;demo2.html&#39;);
        }

        function open2(){
            window.open(&#39;demo2.html&#39;,&#39;_self&#39;);
        }

        function open3(){
            window.open(&#39;demo2.html&#39;,&#39;_blank&#39;);
        }

        function open4(){
            window.open(&#39;demo2.html&#39;,&#39;ot&#39;);
        }

        function open5(){
            window.open(&#39;demo2.html&#39;,&#39;_blank&#39;,&#39;width=200, height=200&#39;);
        }

        // 윈도우 브라우저 보안에 관해서

    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><p><strong>2</strong></p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Document&lt;/title&gt;
    &lt;script src=&quot;../jquery.js&quot;&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;ul&gt;
        &lt;li class=&quot;active&quot;&gt;HTML&lt;/li&gt;
        &lt;li class=&quot;active&quot;&gt;CSS&lt;/li&gt;
        &lt;li class=&quot;active&quot; id=&quot;active&quot;&gt;자바스크립트&lt;/li&gt;
    &lt;/ul&gt;
    &lt;script&gt;
        // dom으로 제어하기 위해서는 제어 대상을 찾아야한다
        // documents.getElementsByTagName
        var ul =document.getElementsByTagName(&#39;ul&#39;)[0];
        var lis= ul.getElementsByTagName(&#39;li&#39;);
        for(var i=0 ;i&lt;lis.length;++i){
            lis[i].style.color=&#39;red&#39;;
        }
        // 위의 방식대로 범위를 좁히는 것이 중요하다


        // 다른 방식
        var liss= document.getElementsByClassName(&#39;active&#39;);
        for(var k in liss){
            // liss[k].style.color=&#39;black&#39;;
        }

        // getElementById 결과는 항상 1개만 나옴
        var aa = document.getElementById(&#39;active&#39;);
        aa.style.color=&#39;green&#39;;

        // querySelector
        var li = document.querySelector(&#39;li&#39;);
        li.style.color=&#39;red&#39;;
        var li = document.querySelector(&#39;.active&#39;)
        li.style.color=&#39;blue&#39;;

        // # = id  , . = class  ul&gt;li(ul 안에있는 li) a[href=&#39;#&#39;]

        // jQuery의 사용
        jQuery(document).ready(function($){
            $(&#39;body&#39;).prepend(&#39;&lt;h1&gt;Hello world!&lt;/h1&gt;&#39;);
        });

        // 제이쿼리는 항상 $로 시작 $는 제이쿼리 함수를 의미한다
        // 가장 많이 받은 인자값은 css selector값이다
        $(&#39;li&#39;).css(&#39;color&#39;,&#39;red&#39;);
        // jquery로 인해서 리턴된 결과물을 제이쿼리 객체라 부른다
        // &lt;li style=&#39;color:red&#39;&gt;&lt;/li&gt; 를 의미함
        $(&#39;.active&#39;).css(&#39;color&#39;,&#39;black&#39;);
        $(&#39;#active&#39;).css(&#39;color&#39;,&#39;green&#39;).css(&#39;textDecoration&#39;,&#39;underline&#39;);
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><p><strong>3</strong></p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;script src=&quot;../jquery.js&quot;&gt;&lt;/script&gt;
    &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;ul&gt;
        &lt;li id=&quot;anchor&quot;&gt;HTML&lt;/li&gt;
        &lt;li id=&quot;active&quot;&gt;CSS&lt;/li&gt;
        &lt;li id=&quot;list&quot;&gt; 자바스크립트&lt;/li&gt;
        &lt;li id=&quot;obj&quot;&gt;&lt;/li&gt;
        &lt;input type=&quot;button&quot; value=&quot;button&quot; id=&quot;button&quot;&gt;
    &lt;/ul&gt;
    &lt;script&gt;
        var li=document.getElementById(&#39;active&#39;);
        console.log(li.constructor.name);
        var lis= document.getElementsByTagName(&#39;li&#39;);
        console.log(lis.constructor.name);

        var target=document.getElementById(&#39;list&#39;);
        console.log(target.constructor.name);
        var target=document.getElementById(&#39;anchor&#39;);
        console.log(target.constructor.name);

        var target = document.getElementById(&#39;button&#39;);
        console.log(target.constructor.name);

        console.group(&#39;before&#39;);
        var lis = document.getElementsByTagName(&#39;li&#39;);
        for(var v=0;v&lt;lis.length;++v){
            console.log(lis[v]);
        }
        console.groupEnd();
        console.group(&#39;after&#39;);
        lis[1].parentNode.removeChild(lis[1]);
        var lis = document.getElementsByTagName(&#39;li&#39;);
        for(var v=0;v&lt;lis.length;++v){
            console.log(lis[v]);
        }
        console.groupEnd();

        var list = $(&#39;li&#39;);
        list.css(&#39;color&#39;,&#39;red&#39;).css(&#39;text-decoration&#39;,&#39;underline&#39;)

        var li = $(&#39;li&#39;);
        console.log(li[0].constructor)
        // 돔 객체는 jquery 메서드를 사용 못하므로 제이쿼리객체안에 넣어야한다
        // 이후 css 적용
        $(li[0]).css(&#39;color&#39;,&#39;black&#39;);

        // 맵을 통해 제이쿼리 살펴보ㄱ,
        var li = $(&#39;li&#39;);
        li.map(function(index,elem){
            console.log(index,elem);
            $(elem).css(&#39;color&#39;,&#39;blue&#39;);
        });

        var obg={1:&#39;lee&#39;};
        $(obg).map(function(index,ele){
            for(var key in ele){
                console.log(11);
                console.log(obg[key]);
            }
            $(&#39;#obj&#39;).text(obg[key]).css(&#39;color&#39;,&#39;red&#39;);
        })
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[스프링으로 예외 / 반복 처리]]></title>
            <link>https://velog.io/@java_dong/%EC%8A%A4%ED%94%84%EB%A7%81%EC%9C%BC%EB%A1%9C-%EC%98%88%EC%99%B8-%EB%B0%98%EB%B3%B5-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@java_dong/%EC%8A%A4%ED%94%84%EB%A7%81%EC%9C%BC%EB%A1%9C-%EC%98%88%EC%99%B8-%EB%B0%98%EB%B3%B5-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Thu, 20 Feb 2025 07:21:25 GMT</pubDate>
            <description><![CDATA[<h1 id="체크예외와-인터페이스">체크예외와 인터페이스</h1>
<p>서비스 계층은 가급적 특정 구현 기술에 의존하지 않고 순수하게 유지하는 것이 좋다. 예를 들어서 서비스가 처리할 수 없는 <code>SQlException</code>에 대한 의존을 제거하려면 어떻게 해야할까?</p>
<p>서비스가 처리할 수 없으므로 리포지토리가 던지는 체크예외인 <code>SQLException</code>을 런타임예외로 전환해서 서비스 계층에 던지자. 이렇게 하면 서비스 계층이 예외를 무시할수 있기 때문에 특정 기술에 의존하는 부분을 제거할 수 있다.</p>
<p><strong>런타임 예외 적용 - 인터페이스</strong></p>
<pre><code> public interface MemberRepository {
      Member save(Member member);
      Member findById(String memberId);
      void update(String memberId, int money);
      void delete(String memberId);
}</code></pre><p><strong>MyDbException 런타임 예외</strong></p>
<pre><code>public class MyDbException extends RuntimeException {
      public MyDbException() {
      }
      public MyDbException(String message) {
          super(message);
    }
      public MyDbException(String message, Throwable cause) {
          super(message, cause);
    }
      public MyDbException(Throwable cause) {
          super(cause);
    }
 }   </code></pre><p><strong>MemberRepositoryV4_1</strong></p>
<pre><code>@Slf4j
  public class MemberRepositoryV4_1 implements MemberRepository {
      private final DataSource dataSource;
      public MemberRepositoryV4_1(DataSource dataSource) {
          this.dataSource = dataSource;
    }

     @Override
      public Member save(Member member) {
          String sql = &quot;insert into member(member_id, money) values(?, ?)&quot;;
          Connection con = null;
          PreparedStatement pstmt = null;
          try {
              con = getConnection();
               pstmt = con.prepareStatement(sql);
               pstmt.setString(1, member.getMemberId());
               pstmt.setInt(2, member.getMoney());
               pstmt.executeUpdate();
              return member;
        } catch (SQLException e) {
            throw new MyDbException(e);
        } finally {
            close(con, pstmt, null);
        }
    }

    @Override
    public void update(String memberId, int money) {
        String sql = &quot;update member set money=? where member_id=?&quot;;
        Connection con = null;
           PreparedStatement pstmt = null;
        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            throw new MyDbException(e);
    }     finally {
            close(con, pstmt, null);
        } 
    }
    .
    .
    .
   private void close(Connection con, Statement stmt, ResultSet rs) {
          JdbcUtils.closeResultSet(rs);
          JdbcUtils.closeStatement(stmt);
          DataSourceUtils.releaseConnection(con, dataSource);
    }
      private Connection getConnection() {
          Connection con = DataSourceUtils.getConnection(dataSource);
          log.info(&quot;get connection={} class={}&quot;, con, con.getClass());
          return con;
    } 
}
</code></pre><ul>
<li>이 코드에서 핵심은 <code>SQLException</code>이라는 체크예외를 <code>MyDbException</code>이라는 런타임 예외로 변환해서 던지는 부분이다</li>
</ul>
<p><strong>예외변환</strong></p>
<pre><code>catch (SQLException e) {
      throw new MyDbException(e);
}</code></pre><ul>
<li>잘보면 기존 예외를 생성자를 통해서 포함하고 있는 것을 확인할 수 있다. 예외는 원인이 되는 예외를 내부에 포함할 수 있는데 이렇게 해야 원인이되는 기존 예외도 함께 확인 할 수있다</li>
</ul>
<p><strong>MemberService4</strong></p>
<pre><code> @Slf4j
  @RequiredArgsConstructor
  public class MemberServiceV4 {
      private final MemberRepository memberRepository;
      @Transactional
      public void accountTransfer(String fromId, String toId, int money) {
          bizLogic(fromId, toId, money);
      }

      private void bizLogic(String fromId, String toId, int money) {
         Member fromMember = memberRepository.findById(fromId);
         Member toMember = memberRepository.findById(toId);
         memberRepository.update(fromId, fromMember.getMoney() - money);
         validation(toMember);
         memberRepository.update(toId, toMember.getMoney() + money);
     }
    private void validation(Member toMember) {
        if (toMember.getMemberId().equals(&quot;ex&quot;)) {
            throw new IllegalStateException(&quot;이체중 예외 발생&quot;); 
            }
    } 
}</code></pre><ul>
<li>이전 버전과 달리 메서드에서 <code>throws SQLException</code> 부분이 제거된 것을 확인할 수 있다</li>
</ul>
<p><br><br></p>
<h1 id="데이터-접근-예외-만들기">데이터 접근 예외 만들기</h1>
<p>데이터베이스 오류에 따라서 특정예외는 복구하고 싶을 수도 있다. 예를 들어 새롭게 회원가입을 시도했는데 기존에 존재하던 ID라서 임의의 숫자를 붙여서 다시 저장될 수 있게 만드는 것이다.</p>
<p>데이터를 DB에 저장할 때 같은 ID가 이미 데이터베이스에 저장되어 있다면 데이터베이스는 오류코드를 반환하고 이 오류 코드를 받은 JDBC 드라이버는 <code>SQLException</code>을 던진다. 그리고 <code>SQLException</code>에는 데이터베이스가 제공하는 <code>errorCode</code>라는 것이 들어있다</p>
<p><code>SQLException</code> 내부에 들어있는 <code>errorCode</code> 를 활용하면 데이터베이스에서 어떤 문제가 발생했는지 확인할 수 있 다</p>
<p><strong>H2 예시</strong></p>
<ul>
<li><code>23505</code>: 키 중복 오류</li>
<li><code>42000</code>: SQL 문법 오류</li>
</ul>
<p>서비스 계층에서는 예외 복구를 위해 키 중복 오류를 확인 할 수 있어야한다. 그래야 새로운 ID를 만들어서 다시 저장을 시도할 수 있기 때문이다. 이러한 과정이 예외를 확인해서 복구하는 과정으로 리포지토리는 <code>SQLException</code>을 서비스 계층에 던지고 서비스 계층은 이 예외의 오류코드를 확인해서 키 중복 오류인 경우 새로운 ID를 만들어서 다시 저장하면 된다. 하지만 <code>SQLException</code>에 들어있는 오류코드를 활용하기 위해 <code>SQLException</code>을 서비스 계층으로 던지면 서비스 계층이 JDBC 기술의 의존하게 되면서 서비스 계층의 순수성이 무너진다. 이 문제를 해결하려면 앞서 해던것처럼 예외를 변환해서 던지면 된다.</p>
<p><strong>변환을 위한 예외 생성 - MyDuplicatekeyException</strong></p>
<pre><code>public class MyDuplicateKeyException extends MyDbException {
      public MyDuplicateKeyException() {
      }
      public MyDuplicateKeyException(String message) {
          super(message);
    }
      public MyDuplicateKeyException(String message, Throwable cause) {
          super(message, cause);
    }
      public MyDuplicateKeyException(Throwable cause) {
          super(cause);
    } 
}</code></pre><ul>
<li>기존에 사용했던 <code>MyDbException</code>을 상속받아서 의미있는 계층을 형성한다. 이렇게 하면 데이터베이스 관련 예외라는 계층을 만들 수 있다</li>
<li>이 예외는 데이터 중복 발생시에만 던져야 한다</li>
<li>이 예외는 사용자 정의 예외로 특정기술에 종속되지 않는다</li>
</ul>
<p><strong>Test 예시</strong></p>
<pre><code>static class Service {
    private final Repository repository;
    public void create(String memberId) {
        try {
            repository.save(new Member(memberId, 0));
            log.info(&quot;saveId={}&quot;, memberId);
        } catch (MyDuplicateKeyException e) {
            log.info(&quot;키 중복, 복구 시도&quot;);
            String retryId = generateNewId(memberId);
            log.info(&quot;retryId={}&quot;, retryId); 
            repository.save(new Member(retryId, 0));
        } catch (MyDbException e) { 
            log.info(&quot;데이터 접근 계층 예외&quot;, e); 
            throw e;
        } 
    }
    private String generateNewId(String memberId) {
        return memberId + new Random().nextInt(10000);
    }
}
</code></pre><p><br><br></p>
<h1 id="스프링-예외-추상화-이해">스프링 예외 추상화 이해</h1>
<p>스프링은 데이터 접근과 관련된 예외를 추상화해서 제공한다</p>
<p><img src="https://velog.velcdn.com/images/java_dong/post/39c7c631-9b9a-477a-b017-c7f02c50b2b8/image.png" alt=""></p>
<ul>
<li>스프링은 데이터 접근 계층에 대한 수십개의 예외를 정리해서 일관된 예외 계층을 제공</li>
<li>각각의 예외는 특정 기술에 종속적이지 않게 설계됨</li>
<li>JDBC나 JPA를 사용할 때는 예외를 스프링이 제공하는 예외로 변환해주는 역할도 제공</li>
<li>런타임 예외를 상속받아서 스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외이다</li>
</ul>
<h2 id="스프링이-제공하는-예외-변환기">스프링이 제공하는 예외 변환기</h2>
<pre><code>@Slf4j
public class SpringExceptionTranslatorTest {
    DataSource dataSource;
    @BeforeEach
    void init() {
        dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    }
    @Test
    void sqlExceptionErrorCode() {
        String sql = &quot;select bad grammar&quot;;
        try {
            Connection con = dataSource.getConnection();
            PreparedStatement stmt = con.prepareStatement(sql);
            stmt.executeQuery();
        } catch (SQLException e) {
            assertThat(e.getErrorCode()).isEqualTo(42122);
            int errorCode = e.getErrorCode();
            log.info(&quot;errorCode={}&quot;, errorCode);
            //org.h2.jdbc.JdbcSQLSyntaxErrorException
            log.info(&quot;error&quot;, e);
        } 
    }
}
</code></pre><ul>
<li>기존에 사용했던 데이터베이스 오류 코드 확인</li>
</ul>
<p><strong>스프링이 제공하는 변환기</strong></p>
<pre><code>  SQLExceptionTranslator exTranslator = new
  SQLErrorCodeSQLExceptionTranslator(dataSource);
  DataAccessException resultEx = exTranslator.translate(&quot;select&quot;, sql, e);</code></pre><ul>
<li><code>translate()</code> 메서드의 첫번째 파라미터는 읽을 수 있는 설명이고 두번째는 실행한 sql, 마지막은 발생된 <code>SQLException</code>을 전달하면된다. 이렇게 하면 스프링 데이터 접근 계층의 예외로 변환해서 반환해 준다</li>
</ul>
<pre><code>  @Test
  void exceptionTranslator() {
      String sql = &quot;select bad grammar&quot;;
      try {
          Connection con = dataSource.getConnection();
          PreparedStatement stmt = con.prepareStatement(sql);
          stmt.executeQuery();
      } catch (SQLException e) {
          assertThat(e.getErrorCode()).isEqualTo(42122);
          //org.springframework.jdbc.support.sql-error-codes.xml
          SQLExceptionTranslator exTranslator = new
           SQLErrorCodeSQLExceptionTranslator(dataSource);
          //org.springframework.jdbc.BadSqlGrammarException
          DataAccessException resultEx = exTranslator.translate(&quot;select&quot;, sql, e);
          log.info(&quot;resultEx&quot;, resultEx);
            assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
      }
}</code></pre><ul>
<li>이 예제에서 눈에 보이는 타입은 <code>DataAccessException</code>이지만 실제로는 <code>BasSqlGrammarException</code>이 반환됨</li>
</ul>
<h2 id="스프링-예외-추상화-적용">스프링 예외 추상화 적용</h2>
<pre><code>
  @Slf4j
  public class MemberRepositoryV4_2 implements MemberRepository {
      private final DataSource dataSource;
      private final SQLExceptionTranslator exTranslator;

      public MemberRepositoryV4_2(DataSource dataSource) {
          this.dataSource = dataSource;
          this.exTranslator = new
              SQLErrorCodeSQLExceptionTranslator(dataSource);
      }

      @Override
      public Member save(Member member) {
            String sql = &quot;insert into member(member_id, money) values(?, ?)&quot;;
            Connection con = null;
            PreparedStatement pstmt = null;
            try {
                con = getConnection();
                pstmt = con.prepareStatement(sql);
                pstmt.setString(1, member.getMemberId());
                pstmt.setInt(2, member.getMoney());
                pstmt.executeUpdate();
                return member;
            } catch (SQLException e) {
                throw exTranslator.translate(&quot;save&quot;, sql, e);
            } finally {
                close(con, pstmt, null);
        }
    }
    .
    .
    .
}
</code></pre><p><br><br><br><br>
출처: <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바의 예외와 DB]]></title>
            <link>https://velog.io/@java_dong/%EC%9E%90%EB%B0%94%EC%9D%98-%EC%98%88%EC%99%B8%EC%99%80-DB</link>
            <guid>https://velog.io/@java_dong/%EC%9E%90%EB%B0%94%EC%9D%98-%EC%98%88%EC%99%B8%EC%99%80-DB</guid>
            <pubDate>Mon, 17 Feb 2025 09:40:06 GMT</pubDate>
            <description><![CDATA[<h1 id="예외-계층">예외 계층</h1>
<p><img src="https://velog.velcdn.com/images/java_dong/post/aa6c3b00-85d6-4079-8683-601dbfb3e09e/image.png" alt=""></p>
<ul>
<li><code>Object:</code>예외도 객체이다. 머든 객체의 최상위 부모가 <code>Object</code>이므로 예외의 최상위 부모도<code>Object</code>이다</li>
<li><code>Throwable:</code>최상위 예외이다. 하위에 <code>Exception</code>,<code>Error</code>가 있다</li>
<li><code>Error:</code> 메모리 부족이나 심각한 시스템 오류와 같이 에플리케이션에서 복구 불가능한 시스템 예외이다.</li>
<li><code>Exception:</code>체크 예외<ul>
<li>에플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다</li>
<li><code>Exception</code>과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다. 단 <code>RuntimeException</code>은 이에 해당하지 않는다</li>
</ul>
</li>
<li><code>RuntimeException:</code>언체크 예외, 런타임 예외<ul>
<li>컴파일러가 체크하지 않는 예외이다</li>
<li><code>RuntimeException</code>과 그 자식 예외는 모두 언체크 예외이다</li>
<li><code>RuntimeException</code>의 이름을 따라서 <code>RuntimeException</code>과 그 하위 언체크 예외를 런타임 예외라고 부른다</li>
</ul>
</li>
</ul>
<p><br><br></p>
<h1 id="기본예외">기본예외</h1>
<p><strong>예외에서는 2가지 규칙을 기억하자</strong></p>
<ul>
<li><p>예외는 잡아서 처리하거나 던져야 한다</p>
</li>
<li><p>예외를 잡거나 던질 때 지정한 예외뿐만 아니라 그 예외의  자식들도 함께 처리된다</p>
<ul>
<li>예를들어 <code>Exception</code>을 <code>catch</code>로 잡으면 그 하위 예외들도 모두 잡을 수 있다</li>
</ul>
</li>
<li><p>자바 <code>main()</code> 스레드의 경우 예외 로그를 출력하면서 시스템이 종료</p>
</li>
<li><p>웹 애플리케이션의 경우 여러 사용자의 요청을 처리하기 때문에 하나의 예외 때문에 시스템이 종료되면 안된다. WAS가 해당 예외를 받아서 처리하는데, 주로 사용자에게 개발자가 지정한, 오류 페이지를 보여준다</p>
</li>
</ul>
<p><br><br></p>
<h1 id="체크-예외-기본-이해">체크 예외 기본 이해</h1>
<ul>
<li><code>Exception</code>과 그 하위 예외는 모두 컴파일러가 체크하는 체크예외이며 이때 <code>RuntimeException</code>은 예외로 한다</li>
<li>체크예외는 잡아서 처리하거나 밖으로 던지도록 선언해야한다. 그러지 않으면 컴파일러에서 오류가 발생한다</li>
</ul>
<p><strong>체크 예외 코드</strong></p>
<pre><code>@Slf4j
  public class CheckedTest {
      @Test
      void checked_catch() {
          Service service = new Service();
          service.callCatch();
      }
      @Test
      void checked_throw() {
          Service service = new Service();
          assertThatThrownBy(() -&gt; service.callThrow())
                  .isInstanceOf(MyCheckedException.class);
}
/**
* Exception을 상속받은 예외는 체크 예외가 된다. */
      static class MyCheckedException extends Exception {
          public MyCheckedException(String message) {
              super(message);
          }
    }

     static class Service {
          Repository repository = new Repository();
/**
* 예외를 잡아서 처리하는 코드 */
      public void callCatch() {
              try {
                  repository.call();
              } catch (MyCheckedException e) {
//예외 처리 로직
                log.info(&quot;예외 처리, message={}&quot;, e.getMessage(), e); }
             }    
      public void callThrow() throws MyCheckedException {
              repository.call();
            } 
    }
      static class Repository {
          public void call() throws MyCheckedException {
              throw new MyCheckedException(&quot;ex&quot;);
          }
    } 
}</code></pre><p><strong>Exception을 상속받은 예외</strong></p>
<pre><code>  static class MyCheckedException extends Exception {
      public MyCheckedException(String message) {
          super(message);
      }
  }</code></pre><ul>
<li><code>MyCheckedException</code>는 <code>Exception</code>을 상속받았다. <code>Exception</code>을 상속받으면 체크예외가 됨</li>
<li><code>RuntimeException</code>을 상속 받으면 언체크 예외가 됨</li>
</ul>
<p><strong>try~catch 예외처리</strong></p>
<pre><code>  @Test
  void checked_catch() {
      Service service = new Service();
      service.callCatch();
  }</code></pre><ul>
<li><code>service.callCatch()</code> 에서 예외를 처리했기 때문에 테스트 메서드까지 예외가 올라오지 않는다</li>
</ul>
<pre><code>public void callCatch() {
      try {
          repository.call();
      } catch (Exception e) {
        //예외 처리 로직 
      }
}
</code></pre><ul>
<li><code>catch</code>에 <code>MyCheckedException</code>의 상위 타입인 <code>Exception</code>을 적어도 <code>MyCheckedException</code>을 잡을 수 있다</li>
<li><code>catch</code>에 예외를 지정하면 해당 예외와 그 하위 타입 예외를 모두 잡아줌</li>
<li><code>throws</code>로 예외를 밖으로 던져버리는 전략도 물론 사용가능하다</li>
</ul>
<p><strong>체크예외의 장단점</strong></p>
<p>체크 예외는 잡아서 처리할수 없을 때 예외를 밖으로 던지는 <code>throws</code>예외를 필수로 선언해야 함. 그렇지 않으면 컴파일 오류가 발생해 장단점이 동시에 존재한다</p>
<p><br><br></p>
<h1 id="언체크-예외-기본-이해">언체크 예외 기본 이해</h1>
<ul>
<li>언체크 예외는 기본적으로 컴파일러가 체크하지 않는다</li>
<li>언체크 예외는 기본적으로 체크 예외와 동일하지만 차이가 있다면, <code>throws</code>를 선언하지 않고 생략이 가능하다는 점이다</li>
</ul>
<br>

<p><strong>언체크 예외</strong></p>
<pre><code> static class MyUncheckedException extends RuntimeException {
        public MyUncheckedException(String message) {
            super(message);
        }
}</code></pre><p><strong>언체크 예외를 잡아서 처리하는 코드</strong></p>
<pre><code> try {
      repository.call();
} catch (MyUncheckedException e) { //예외 처리 로직
      log.info(&quot;error&quot;, e);
  }</code></pre><p><strong>언체크 예외를 밖으로 던지는 코드</strong></p>
<pre><code>  public void callThrow() {
      repository.call();
}</code></pre><ul>
<li>언체크 예외는 말그대로 컴파일러가 체크하지않는 예외이기 때문에 <code>throws</code> 선언을 하지 않아도 됨</li>
</ul>
<br>

<p><strong>언체크 예외 장단점</strong></p>
<p>장점: <code>throws</code> 생략
단점: 컴파일러가 체크를 하지 않아 개발자가 누락할 가능성이 있음</p>
<p><br><br></p>
<h1 id="체크예외-활용">체크예외 활용</h1>
<p><strong>기본원칙 2가지</strong></p>
<ul>
<li>기본적으로 런타임 예외를 사용</li>
<li>체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용<ul>
<li>이 경우 해당 예외를 잡아서 반드시 처리해야하는 문제일때만 사용해야 함<ul>
<li>ex) 계좌이체 실패 예외</li>
<li>결제시 포인트 부족 실패 예외</li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<p><strong>체크예외의 문제점</strong></p>
<p><img src="https://velog.velcdn.com/images/java_dong/post/82f9b2d8-a72f-4030-9aa4-754176fe4b3f/image.png" alt=""></p>
<ul>
<li><p>복구 불가능한 예외</p>
<ul>
<li>대부분의 예외는 복구가 불가능하다. <code>SQLException</code>로 예를 들자면 데이터베이스에 문제가 있어서 발생하는 예외이다. sql상 문법 오류 또는 데이터베이스 서버 다운등의 문제가 발생했을 수 있다. 이런 문제들은 대부분 복구가 불가능하다. 또한 대부분의 서비스나 컨트롤러는 이런 문제를 해결할 수 없다. 따라서 이런 문제들은 일관성있게 공통으로 처리해야 한다. 서블릿 필터, 스프링 인터셉터, 스프링의 <code>ControllerAdvice</code> 를 사용하면 이런 부분을 깔끔하게 공통으로 해결할 수 있다</li>
</ul>
</li>
<li><p>의존관계에 대한 문제</p>
<ul>
<li>체크 예외의 심각한 문제는 예외에 대한 의존 문제이다. 체크 예외는 대부분 해결 불가능한 오류이므로 <code>throws</code>로 던지면 서비스, 컨트롤러 , 레포지토리들도 해당 예외를 모두 알아야한다. 그래서 불필요한 의존관계가 발생하게된다</li>
</ul>
</li>
</ul>
<ul>
<li>이런 문제가 발생하는 걸 막기 위해 <code>throw Exception</code>을 쓸수도 있는데 이는 위험한 결정이다. 
사용자가 의도한 모든 오류를 던지기 때문에 어떤 오류가 발생했는지 모르게 될 가능성이 크다</li>
</ul>
<p><br><br></p>
<h1 id="언체크-예외-활용">언체크 예외 활용</h1>
<p><img src="https://velog.velcdn.com/images/java_dong/post/95290223-0aba-4c40-b604-d530b5b9c03d/image.png" alt=""></p>
<ul>
<li><code>SQLException</code> 을 런타임 예외인 <code>RuntimeSQLException</code> 으로 변환했다</li>
<li><code>ConnectException</code> 대신에 <code>RuntimeConnectException</code> 을 사용하도록 바꾸었다</li>
<li>런타임 예외이기 때문에 서비스, 컨트롤러는 해당 예외들을 처리할 수 없다면 별도의 선언 없이 그냥 두면 된다</li>
</ul>
<p><strong>예외 전환</strong></p>
<ul>
<li>레포지토리에서 체크 예외인 <code>SqlExcption</code>이 발생하면 런타임 예외인 <code>RuntimeSQLException</code>으로 전환해서 예외를 던진다<ul>
<li>참고로 이때 기존 예외를 포함해야지 출력시 기존 예외도 함께 확인이 가능하다</li>
</ul>
</li>
<li><code>NetworkClient</code> 는 단순히 기존 체크 예외를 <code>RuntimeConnectException</code> 이라는 런타임 예외가 발생하
도록 코드를 바꾸었다</li>
</ul>
<p><strong>복구 불가능한 문제</strong></p>
<p>시스템에서 발생한 예외는 대부분 복구 불가능 예외이다. 런타임 예외를 사용하면 서비스나 컨트롤러가 이런 복구 불가 능한 예외를 신경쓰지 않아도 된다. 물론 이렇게 복구 불가능한 예외는 일관성 있게 공통으로 처리해야 한다</p>
<p><strong>의존관계 처리</strong></p>
<p>런타임 예외는 해당 객체가 처리할 수 없는 예외는 무시하면 된다. 따라서 체크 예외 처럼 예외를 강제로 의존하지 않아 도 된다</p>
<p><strong>기존예외를 포함</strong></p>
<pre><code> public void call() {
      try {
          runSQL();
      } catch (SQLException e) {

        throw new RuntimeSQLException(e); 
        //기존 예외(e) 포함 
      }
}</code></pre><p><strong>기존예외 포함 x</strong></p>
<pre><code>  public void call() {
      try {
          runSQL();
      } catch (SQLException e) {
        throw new RuntimeSQLException(); 
        //기존 예외(e) 제외
      }
}
</code></pre><p><br><br><br><br>
출처: <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1</a></p>
]]></description>
        </item>
    </channel>
</rss>